diff --git a/accord-cluster-debug/build.gradle b/accord-cluster-debug/build.gradle new file mode 100644 index 0000000000..5859b766b6 --- /dev/null +++ b/accord-cluster-debug/build.gradle @@ -0,0 +1,43 @@ +/* + * 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. + */ + +plugins { + id 'accord.java-conventions' + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':accord-core') + implementation project(':accord-debug') + implementation 'io.javalin:javalin:5.6.2' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.apache.cassandra:cassandra-driver-core:3.12.1' + implementation 'org.slf4j:slf4j-api:1.7.36' + + runtimeOnly 'ch.qos.logback:logback-classic:1.2.12' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + +} + +application { + mainClass = 'accord.cluster.debug.server.ClusterDebugServer' +} \ No newline at end of file diff --git a/accord-cluster-debug/debug-config.json b/accord-cluster-debug/debug-config.json new file mode 100644 index 0000000000..9599ae340b --- /dev/null +++ b/accord-cluster-debug/debug-config.json @@ -0,0 +1,20 @@ +{ + "hosts": [ + { + "host": "127.0.0.1", + "port": 9042 + }, + { + "host": "127.0.0.2", + "port": 9042 + }, + { + "host": "127.0.0.3", + "port": 9042 + } + ], + "server": { + "port": 8081, + "host": "0.0.0.0" + } +} diff --git a/accord-cluster-debug/src/main/java/accord/cluster/debug/controller/ExternalClusterController.java b/accord-cluster-debug/src/main/java/accord/cluster/debug/controller/ExternalClusterController.java new file mode 100644 index 0000000000..4da433a357 --- /dev/null +++ b/accord-cluster-debug/src/main/java/accord/cluster/debug/controller/ExternalClusterController.java @@ -0,0 +1,574 @@ +/* + * 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 accord.cluster.debug.controller; + +import accord.cluster.debug.server.ExclusiveConnection; +import accord.debug.Response; +import accord.debug.controller.Controller; +import accord.debug.model.*; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class ExternalClusterController implements Controller +{ + private static final Logger logger = LoggerFactory.getLogger(ExternalClusterController.class); + + private final DebugServerConfig config; + private final Map exclusiveConnections = new ConcurrentHashMap<>(); + + public ExternalClusterController(DebugServerConfig config) + { + this.config = config; + initializeExclusiveConnections(); + } + + private void initializeExclusiveConnections() + { + if (config == null || config.getHosts() == null) + { + logger.warn("No host configuration found, skipping exclusive connections setup"); + return; + } + + for (DebugServerConfig.HostConfig hostConfig : config.getHosts()) + { + try + { + Cluster exclusiveCluster = ExclusiveConnection.session(builder -> builder.withPort(hostConfig.port), hostConfig.host); + exclusiveConnections.put(hostConfig.toString(), exclusiveCluster); + logger.info("Created exclusive connection to {} ({}:{})", + hostConfig, hostConfig.host, hostConfig.port); + } + catch (Exception e) + { + logger.error("Failed to create exclusive connection to {} ({}:{}): {}", + hostConfig.toString(), hostConfig.host, hostConfig.port, e.getMessage()); + } + } + } + + @Override + public List getNodes() + { + for (Map.Entry e : exclusiveConnections.entrySet()) + { + + new NodeInfo(e.getKey(), + new StoreInfo()) + } + return List.of(); + } + + private static final String REDUNDANT_BEFORE_QUERY = + "SELECT keyspace_name, table_name, table_id, token_start, token_end, " + + "command_store_id, start_epoch, end_epoch, gc_before, shard_applied, " + + "quorum_applied, locally_applied, locally_durable_to_command_store, " + + "locally_durable_to_data_store, locally_redundant, locally_synced, " + + "locally_witnessed, pre_bootstrap, stale_until_at_least " + + "FROM system_accord_debug.redundant_before"; + + private static final String COORDINATIONS_QUERY = + "SELECT txn_id, kind, coordination_id, description, nodes, " + + "nodes_inflight, nodes_contacted, participants, replies, tracker " + + "FROM system_accord_debug.coordinations"; + + private static final String TRANSACTION_SEARCH_QUERY = + "SELECT command_store_id, txn_id, save_status, route, durability, " + + "execute_at, executes_at_least, txn, deps, waiting_on, writes, result, " + + "participants_owns, participants_touches, participants_has_touched, " + + "participants_executes, participants_waits_on " + + "FROM system_accord_debug.txn WHERE txn_id = ?"; + + private static final String TXN_BLOCKED_BY_QUERY = + "SELECT txn_id, keyspace_name, table_name, command_store_id, depth, " + + "blocked_by, reason, save_status, execute_at, key " + + "FROM system_accord_debug.txn_blocked_by WHERE txn_id = ?"; + + private static final String PROGRESS_LOG_QUERY = + "SELECT keyspace_name, table_name, table_id, command_store_id, txn_id, " + + "contact_everyone, waiting_is_uninitialised, waiting_blocked_until, " + + "waiting_home_satisfies, waiting_progress, waiting_retry_counter, " + + "waiting_packed_key_tracker_bits, waiting_scheduled_at, home_phase, " + + "home_progress, home_retry_counter, home_scheduled_at " + + "FROM system_accord_debug.progress_log"; + + private static final String DURABILITY_SERVICE_QUERY = + "SELECT keyspace_name, table_name, token_start, token_end, " + + "last_started_at, cycle_started_at, retries, min, requested_by, " + + "active, waiting, node_offset, cycle_offset, active_index, " + + "next_index, next_to_index, end_index, current_splits, stopping, stopped " + + "FROM system_accord_debug.durability_service"; + + private static final String COMMAND_STORE_QUERY = + "SELECT command_store_id, ranges " + + "FROM system_accord_debug.command_store"; + + private static final String DURABLE_BEFORE_QUERY = + "SELECT keyspace_name, table_name, token_start, token_end, quorum, universal " + + "FROM system_accord_debug.durable_before"; + + private static final String EPOCHS_QUERY = + "SELECT epoch, ready_metadata, ready_coordinate, ready_data, ready_reads, ready " + + "FROM system_views.accord_epochs"; + + private static final String TABLE_EPOCHS_QUERY = + "SELECT epoch, keyspace_name, table_name, added, removed, synced, closed, retired " + + "FROM system_views.accord_table_epochs"; + + public static Response> getRedundantBefore(Session session) + { + try + { + ResultSet resultSet = session.execute(REDUNDANT_BEFORE_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + RedundantBeforeInfo rb = new RedundantBeforeInfo( + row.getString("keyspace_name"), + row.getString("table_name"), + row.getString("table_id"), + row.getString("token_start"), + row.getString("token_end"), + row.getInt("command_store_id"), + row.getLong("start_epoch"), + row.getLong("end_epoch"), + row.getString("gc_before"), + row.getString("shard_applied"), + row.getString("quorum_applied"), + row.getString("locally_applied"), + row.getString("locally_durable_to_command_store"), + row.getString("locally_durable_to_data_store"), + row.getString("locally_redundant"), + row.getString("locally_synced"), + row.getString("locally_witnessed"), + row.getString("pre_bootstrap"), + row.getString("stale_until_at_least") + ); + results.add(rb); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query redundant_before table: " + e.getMessage()); + } + } + + public static Response> getCoordinations(Session session) + { + try + { + ResultSet resultSet = session.execute(COORDINATIONS_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + CoordinationInfo coordination = new CoordinationInfo( + row.getString("txn_id"), + row.getString("kind"), + row.getLong("coordination_id"), + row.getString("description"), + row.getString("nodes"), + row.getString("nodes_inflight"), + row.getString("nodes_contacted"), + row.getString("participants"), + row.getString("replies"), + row.getString("tracker") + ); + results.add(coordination); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query coordinations table: " + e.getMessage()); + } + } + + public static Response> searchTransactions(Session session, String txnId) + { + try + { + ResultSet resultSet = session.execute(TRANSACTION_SEARCH_QUERY, txnId); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + TxnInfo transaction = new TxnInfo( + row.getInt("command_store_id"), + row.getString("txn_id"), + row.getString("save_status"), + row.getString("route"), + row.getString("durability"), + row.getString("execute_at"), + row.getString("executes_at_least"), + row.getString("txn"), + row.getString("deps"), + null, null, +// TODO: +// row.getString("waiting_on"), +// row.getString("waiting_on"), + row.getString("writes"), + row.getString("result"), + row.getString("participants_owns"), + row.getString("participants_touches"), + row.getString("participants_has_touched"), + row.getString("participants_executes"), + row.getString("participants_waits_on") + ); + results.add(transaction); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to search transactions: " + e.getMessage()); + } + } + + public static Response> getTxnBlockedBy(Session session, String txnId) + { + try + { + ResultSet resultSet = session.execute(TXN_BLOCKED_BY_QUERY, txnId); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + TxnBlockedByInfo blockedBy = new TxnBlockedByInfo( + row.getString("txn_id"), + row.getString("keyspace_name"), + row.getString("table_name"), + row.getInt("command_store_id"), + row.getInt("depth"), + row.getString("blocked_by"), + row.getString("reason"), + row.getString("save_status"), + row.getString("execute_at"), + row.getString("key") + ); + results.add(blockedBy); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query txn_blocked_by table: " + e.getMessage()); + } + } + + /** + * keyspace_name text, + * table_name text, + * table_id text, + * command_store_id int, + * txn_id 'TxnIdUtf8Type', + * + * // Timer + BaseTxnState + * contact_everyone boolean, + * + * // WaitingState + * waiting_is_uninitialised boolean, + * waiting_blocked_until text, + * waiting_home_satisfies text, + * waiting_progress text, + * waiting_retry_counter int, + * waiting_packed_key_tracker_bits text, + * waiting_scheduled_at timestamp, + * + * //HomeState/TxnState + * home_phase text, + * home_progress text, + * home_retry_counter int, + * home_scheduled_at timestamp, + * PRIMARY KEY (keyspace_name, table_name, table_id, command_store_id, txn_id)" + + */ + public static Response> getProgressLog(Session session) + { + try + { + ResultSet resultSet = session.execute(PROGRESS_LOG_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + ProgressLogInfo progressLog = new ProgressLogInfo( + row.getString("keyspace_name"), + row.getString("table_name"), + row.getString("table_id"), + row.getInt("command_store_id"), + row.getString("txn_id"), + row.getBool("contact_everyone"), + row.getBool("waiting_is_uninitialised"), + row.getString("waiting_blocked_until"), + row.getString("waiting_home_satisfies"), + row.getString("waiting_progress"), + row.getInt("waiting_retry_counter"), + row.getString("waiting_packed_key_tracker_bits"), + row.getTimestamp("waiting_scheduled_at") != null ? row.getTimestamp("waiting_scheduled_at").getTime() : 0, + row.getString("home_phase"), + row.getString("home_progress"), + row.getInt("home_retry_counter"), + row.getTimestamp("home_scheduled_at") != null ? row.getTimestamp("home_scheduled_at").getTime() : 0 + ); + results.add(progressLog); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query progress_log table: " + e.getMessage()); + } + } + + public static Response> getDurabilityService(Session session) + { + try + { + ResultSet resultSet = session.execute(DURABILITY_SERVICE_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + DurabilityServiceInfo durabilityService = new DurabilityServiceInfo( + row.getString("keyspace_name"), + row.getString("table_name"), + row.getString("token_start"), + row.getString("token_end"), + row.getLong("last_started_at"), + row.getLong("cycle_started_at"), + row.getInt("retries"), + row.getString("min"), + row.getString("requested_by"), + row.getString("active"), + row.getString("waiting"), + row.getInt("node_offset"), + row.getInt("cycle_offset"), + row.getInt("active_index"), + row.getInt("next_index"), + row.getInt("next_to_index"), + row.getInt("end_index"), + row.getInt("current_splits"), + row.getBool("stopping"), + row.getBool("stopped") + ); + results.add(durabilityService); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query durability_service table: " + e.getMessage()); + } + } + + public static Response> getCommandStore(Session session) + { + try + { + ResultSet resultSet = session.execute(COMMAND_STORE_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + CommandStoreInfo commandStore = new CommandStoreInfo( + row.getInt("command_store_id"), + getList(row, "ranges") + ); + results.add(commandStore); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query command_store table: " + e.getMessage()); + } + } + + private static List getList(Row row, String column) + { + Object safeToReadObj = row.getObject(column); + if (safeToReadObj instanceof List) + return (List) safeToReadObj; + + throw new IllegalStateException(); + } + + private static Map> getMap(Row row, String column) + { + // Linked to preserve iteration order + Map> res = new LinkedHashMap<>(); + + // Try to get as generic object first, then convert + Object safeToReadObj = row.getObject(column); + if (safeToReadObj instanceof Map) + { + @SuppressWarnings("unchecked") + Map rawMap = (Map) safeToReadObj; + + for (Map.Entry entry : rawMap.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof List) + { + @SuppressWarnings("unchecked") + List stringList = (List) value; + res.put(key, stringList); + } + else if (value != null) + { + // Convert single values to single-item lists + res.put(key, Arrays.asList(value.toString())); + } + } + } + return res; + } + + public static Response> getDurableBefore(Session session) + { + try + { + ResultSet resultSet = session.execute(DURABLE_BEFORE_QUERY); + List results = new ArrayList<>(); + + for (Row row : resultSet) + { + DurableBeforeInfo durableBefore = new DurableBeforeInfo( + row.getString("keyspace_name"), + row.getString("table_name"), + row.getString("token_start"), + row.getString("token_end"), + row.getString("quorum"), + row.getString("universal") + ); + results.add(durableBefore); + } + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query durable_before table: " + e.getMessage()); + } + } + + public static Response> getTopologies(Session session) + { + try + { + // Get epochs data + ResultSet epochsResultSet = session.execute(EPOCHS_QUERY); + Map epochsMap = new HashMap<>(); + + for (Row row : epochsResultSet) + { + long epochNum = row.getLong("epoch"); + EpochInfo epoch = new EpochInfo( + epochNum, + row.getString("ready_metadata"), + row.getString("ready_coordinate"), + row.getString("ready_data"), + row.getString("ready_reads"), + row.getBool("ready") + ); + epochsMap.put(epochNum, epoch); + } + + // Get table epochs data + ResultSet tableEpochsResultSet = session.execute(TABLE_EPOCHS_QUERY); + Map> tableEpochsMap = new HashMap<>(); + + for (Row row : tableEpochsResultSet) + { + long epochNum = row.getLong("epoch"); + TableEpoch tableEpoch = new TableEpoch( + epochNum, + row.getString("keyspace_name"), + row.getString("table_name"), + row.getList("added", String.class), + row.getList("removed", String.class), + row.getList("synced", String.class), + row.getList("closed", String.class), + row.getList("retired", String.class) + ); + + tableEpochsMap.computeIfAbsent(epochNum, k -> new ArrayList<>()).add(tableEpoch); + } + + // Combine data into Topology objects + List results = new ArrayList<>(); + Set allEpochs = new HashSet<>(); + allEpochs.addAll(epochsMap.keySet()); + allEpochs.addAll(tableEpochsMap.keySet()); + + for (Long epochNum : allEpochs) + { + EpochInfo epoch = epochsMap.get(epochNum); + List tableEpochs = tableEpochsMap.getOrDefault(epochNum, new ArrayList<>()); + + TopologyInfo topology = new TopologyInfo(epoch, tableEpochs); + results.add(topology); + } + + // Sort by epoch number + results.sort(Comparator.comparingLong(a -> a.epoch != null ? a.epoch.epoch : -1L)); + + return Response.success(results); + } + catch (Exception e) + { + logger.error("Caught an exception in controller", e); + return Response.failure("Failed to query topology tables: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ClusterDebugServer.java b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ClusterDebugServer.java new file mode 100644 index 0000000000..8e5dc90fa2 --- /dev/null +++ b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ClusterDebugServer.java @@ -0,0 +1,502 @@ +/* + * 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 accord.cluster.debug.server; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.cluster.debug.controller.ExternalClusterController; +import accord.debug.model.DebugServerConfig; +import accord.debug.Response; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.javalin.Javalin; +import io.javalin.http.Context; +import io.javalin.http.staticfiles.Location; +import io.javalin.json.JavalinGson; + +public class ClusterDebugServer +{ + private static final Logger logger = LoggerFactory.getLogger(ClusterDebugServer.class); + + private static final AtomicReference instance = new AtomicReference<>(); + + private final Javalin app; + private final int port; + private final DebugServerConfig config; + private final Map exclusiveConnections = new ConcurrentHashMap<>(); + + public ClusterDebugServer(int port, DebugServerConfig config) + { + this.port = port; + this.config = config; + this.app = Javalin.create(appConfig -> { + appConfig.staticFiles.add("/web", Location.CLASSPATH); + appConfig.jsonMapper(createCustomJsonMapper()); + }); + + setupRoutes(); + initializeExclusiveConnections(); + + instance.compareAndSet(null, this); + } + + private static JavalinGson createCustomJsonMapper() + { + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + return new JavalinGson(gson); + } + + private void setupRoutes() + { + app.get("/hosts/{hostname}/redundant_before", this::handleRedundantBefore); + app.get("/hosts/{hostname}/transactions/{txnId}", this::handleTransactionSearch); + app.get("/hosts/{hostname}/coordinations", this::handleCoordinations); + app.get("/hosts/{hostname}/blocked_by/{txnId}", this::handleTxnBlockedBy); + app.get("/hosts/{hostname}/progress_log", this::handleProgressLog); + app.get("/hosts/{hostname}/durability_service", this::handleDurabilityService); + app.get("/hosts/{hostname}/command_store", this::handleCommandStore); // TODO: rename! + app.get("/hosts/{hostname}/durable_before", this::handleDurableBefore); + app.get("/hosts/{hostname}/topologies", this::handleTopologies); + app.get("/hosts", this::handleGetHosts); + } + + public void start() + { + app.start(port); + logger.info("Cluster debug server started on port {}", app.port()); + } + + public void stop() + { + exclusiveConnections.values().forEach(conn -> { + if (!conn.isClosed()) { + conn.close(); + } + }); + exclusiveConnections.clear(); + + app.stop(); + logger.info("Cluster debug server stopped"); + } + + + private void handleRedundantBefore(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getRedundantBefore(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting redundant_before data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleTransactionSearch(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + String txnId = ctx.pathParam("txnId"); + + if (txnId == null || txnId.trim().isEmpty()) + { + Response.sendResponse(ctx, Response.failure("Transaction ID is required"), 400); + return; + } + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.searchTransactions(hostSession, txnId); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error searching transactions for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleCoordinations(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getCoordinations(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting coordinations data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleTxnBlockedBy(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + String txnId = ctx.pathParam("txnId"); + + if (txnId == null || txnId.trim().isEmpty()) + { + Response.sendResponse(ctx, Response.failure("Transaction ID is required"), 400); + return; + } + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getTxnBlockedBy(hostSession, txnId); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting blocked by data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleProgressLog(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getProgressLog(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting progress log data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleDurabilityService(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getDurabilityService(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting durability service data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleCommandStore(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getCommandStore(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting command store data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleDurableBefore(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getDurableBefore(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting durable before data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleTopologies(Context ctx) + { + try + { + String hostname = ctx.pathParam("hostname"); + + Cluster exclusiveCluster = exclusiveConnections.get(hostname); + if (exclusiveCluster == null) + { + Response.sendResponse(ctx, Response.failure("Host not found in configuration: " + hostname), 404); + return; + } + + if (exclusiveCluster.isClosed()) + { + Response.sendResponse(ctx, Response.failure("Connection to host is closed: " + hostname), 503); + return; + } + + try (Session hostSession = exclusiveCluster.connect()) + { + var response = ExternalClusterController.getTopologies(hostSession); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting topologies data for host", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleGetHosts(Context ctx) + { + try + { + if (config == null || config.getHosts() == null) + { + Response.sendResponse(ctx, Response.success(java.util.Collections.emptyList())); + return; + } + + Response.sendResponse(ctx, Response.success(config.getHosts())); + } + catch (Exception e) + { + logger.error("Error getting hosts list", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void initializeExclusiveConnections() + { + if (config == null || config.getHosts() == null) + { + logger.warn("No host configuration found, skipping exclusive connections setup"); + return; + } + + for (DebugServerConfig.HostConfig hostConfig : config.getHosts()) + { + try + { + Cluster exclusiveCluster = ExclusiveConnection.session( + builder -> builder.withPort(hostConfig.port), + hostConfig.host + ); + exclusiveConnections.put(hostConfig.toString(), exclusiveCluster); + logger.info("Created exclusive connection to {} ({}:{})", + hostConfig.toString(), hostConfig.host, hostConfig.port); + } + catch (Exception e) + { + logger.error("Failed to create exclusive connection to {} ({}:{}): {}", + hostConfig.toString(), hostConfig.host, hostConfig.port, e.getMessage()); + } + } + } + + public Map getExclusiveConnections() + { + return exclusiveConnections; + } + + public static void main(String[] args) throws IOException + { + String configPath = args.length > 0 ? args[0] : "debug-config.json"; + int port = args.length > 1 ? Integer.parseInt(args[1]) : 8081; + + DebugServerConfig config = ConfigLoader.loadConfig(configPath); + if (config.getServer() != null && config.getServer().port > 0) + { + port = config.getServer().port; + } + + ClusterDebugServer server = new ClusterDebugServer(port, config); + + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + + server.start(); + logger.info("Cluster debug server running on http://localhost:{}", port); + logger.info("Configuration loaded from: {}", configPath); + logger.info("Exclusive connections established: {}", server.getExclusiveConnections().size()); + logger.info("Available endpoints:"); + logger.info(" GET /hosts//redundant_before - Get redundant_before data for specific host"); + logger.info(" GET /hosts//transactions/ - Search for transaction by ID on specific host"); + logger.info(" GET /hosts//coordinations - Get coordination data for specific host"); + logger.info(" GET /hosts//blocked_by/ - Get blocked by data for transaction on specific host"); + logger.info(" GET /hosts//progress_log - Get progress log data for specific host"); + logger.info(" GET /hosts//durability_service - Get durability service data for specific host"); + logger.info(" GET /hosts//command_store - Get command store data for specific host"); + logger.info(" GET /hosts//durable_before - Get durable before data for specific host"); + logger.info(" GET /hosts//topologies - Get topology information for specific host"); + logger.info("Web Interface:"); + logger.info(" /redundant_before.html?host= - Redundant Before interface"); + logger.info(" /coordinations.html?host= - Coordinations interface"); + logger.info(" /progress_log.html?host= - Progress Log interface"); + logger.info(" /durability_service.html?host= - Durability Service interface"); + logger.info(" /command_store_tmp.html?host= - Command Store interface"); + logger.info(" /durable_before.html?host= - Durable Before interface"); + logger.info(" /topologies.html?host= - Topologies interface"); + } +} \ No newline at end of file diff --git a/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ConfigLoader.java b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ConfigLoader.java new file mode 100644 index 0000000000..2e7e8a48b2 --- /dev/null +++ b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ConfigLoader.java @@ -0,0 +1,89 @@ +/* + * 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 accord.cluster.debug.server; + +import accord.debug.model.DebugServerConfig; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ConfigLoader +{ + private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class); + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public static DebugServerConfig loadConfig(String configPath) throws IOException + { + Path path = Paths.get(configPath); + if (!Files.exists(path)) + { + logger.warn("Config file not found at {}, creating default config", configPath); + DebugServerConfig defaultConfig = createDefaultConfig(); + saveConfig(defaultConfig, configPath); + return defaultConfig; + } + + try (FileReader reader = new FileReader(configPath)) + { + DebugServerConfig config = gson.fromJson(reader, DebugServerConfig.class); + logger.info("Loaded configuration from {}", configPath); + return config; + } + catch (Exception e) + { + logger.error("Failed to load configuration from {}", configPath, e); + throw new IOException("Failed to load configuration: " + e.getMessage(), e); + } + } + + public static void saveConfig(DebugServerConfig config, String configPath) throws IOException + { + try + { + String json = gson.toJson(config); + Files.write(Paths.get(configPath), json.getBytes()); + logger.info("Saved configuration to {}", configPath); + } + catch (Exception e) + { + logger.error("Failed to save configuration to {}", configPath, e); + throw new IOException("Failed to save configuration: " + e.getMessage(), e); + } + } + + private static DebugServerConfig createDefaultConfig() + { + DebugServerConfig.ServerConfig serverConfig = new DebugServerConfig.ServerConfig(8081, "0.0.0.0"); + + java.util.List hosts = java.util.Arrays.asList( + new DebugServerConfig.HostConfig("127.0.0.1", 9042), + new DebugServerConfig.HostConfig("127.0.0.2", 9042), + new DebugServerConfig.HostConfig("127.0.0.3", 9042) + ); + + return new DebugServerConfig(hosts, serverConfig); + } +} \ No newline at end of file diff --git a/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ExclusiveConnection.java b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ExclusiveConnection.java new file mode 100644 index 0000000000..8ebbb3aff7 --- /dev/null +++ b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/ExclusiveConnection.java @@ -0,0 +1,104 @@ +package accord.cluster.debug.server; + +import java.net.InetAddress; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import com.google.common.collect.Iterators; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.HostDistance; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.policies.LoadBalancingPolicy; + +public class ExclusiveConnection +{ + public static Cluster session(Consumer configure, String host) + { + try + { + Cluster.Builder builder = Cluster.builder() + .withCodecRegistry(new CodecRegistry() + .register(PseudoUtf8TypeCodec.TOKEN_CODEC) + .register(PseudoUtf8TypeCodec.TXNID_CODEC)); + configure.accept(builder); + builder.addContactPoint(host); + InetAddress addr = InetAddress.getByName(host); + builder.withLoadBalancingPolicy(new SingleHostLoadBalancingPolicy(addr)); + return builder.build(); + } + catch (Throwable t) + { + throw new RuntimeException("Could not build session", t); + } + } + + public static class SingleHostLoadBalancingPolicy implements LoadBalancingPolicy + { + private final InetAddress address; + private Host host; + + public SingleHostLoadBalancingPolicy(InetAddress address) + { + this.address = address; + } + + protected final List hosts = new CopyOnWriteArrayList<>(); + + @Override + public void init(Cluster cluster, Collection hosts) + { + host = hosts.stream() + .filter(h -> h.getBroadcastAddress().equals(address)).findFirst() + .orElseThrow(() -> new AssertionError("The host should be a contact point")); + this.hosts.add(host); + } + + @Override + public HostDistance distance(Host host) + { + return HostDistance.LOCAL; + } + + @Override + public Iterator newQueryPlan(String loggedKeyspace, Statement statement) + { + return Iterators.singletonIterator(host); + } + + @Override + public void onAdd(Host host) + { + // no-op + } + + @Override + public void onUp(Host host) + { + // no-op + } + + @Override + public void onDown(Host host) + { + // no-op + } + + @Override + public void onRemove(Host host) + { + // no-op + } + + @Override + public void close() + { + // no-op + } + } +} diff --git a/accord-cluster-debug/src/main/java/accord/cluster/debug/server/PseudoUtf8TypeCodec.java b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/PseudoUtf8TypeCodec.java new file mode 100644 index 0000000000..6608718a38 --- /dev/null +++ b/accord-cluster-debug/src/main/java/accord/cluster/debug/server/PseudoUtf8TypeCodec.java @@ -0,0 +1,54 @@ +package accord.cluster.debug.server; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.exceptions.InvalidTypeException; + +public class PseudoUtf8TypeCodec extends TypeCodec +{ + public static final PseudoUtf8TypeCodec TOKEN_CODEC = new PseudoUtf8TypeCodec("org.apache.cassandra.db.marshal.TokenUtf8Type"); + public static final PseudoUtf8TypeCodec TXNID_CODEC = new PseudoUtf8TypeCodec("org.apache.cassandra.db.marshal.TxnIdUtf8Type"); + + private PseudoUtf8TypeCodec(String type) { + super(DataType.custom(type), String.class); + } + + @Override + public ByteBuffer serialize(String value, ProtocolVersion protocolVersion) throws InvalidTypeException + { + if (value == null) { + return null; + } + return ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws InvalidTypeException { + if (bytes == null || bytes.remaining() == 0) { + return null; + } + byte[] array = new byte[bytes.remaining()]; + bytes.duplicate().get(array); + return new String(array, StandardCharsets.UTF_8); + } + + @Override + public String parse(String value) throws InvalidTypeException { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("null")) { + return null; + } + return value; + } + + @Override + public String format(String value) throws InvalidTypeException { + if (value == null) { + return "null"; + } + return "'" + value.replace("'", "''") + "'"; + } +} \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/command_store.html b/accord-cluster-debug/src/main/resources/web/command_store.html new file mode 100644 index 0000000000..fd070ef45e --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/command_store.html @@ -0,0 +1,441 @@ + + + + + + + Cluster Debug Interface - Command Store + + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading command store data... +
+ +
+ No command store data found for {{ selectedHost }} +
+ +
+
+
+ Store ID: {{ entry.commandStoreId }} +
+ +
+
Safe to Read Map:
+
+ (empty map) +
+
+
+
+ +
+
{{ value }}
+
+
+
+ +
+
Ranges for Epoch:
+
+ (empty map) +
+
+ + + + + + + + + + + + + +
EpochRanges
{{ epoch }} +
+
+ {{ range }} +
+
+
+ {{ ranges }} +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/coordinations.html b/accord-cluster-debug/src/main/resources/web/coordinations.html new file mode 100644 index 0000000000..129b3fb496 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/coordinations.html @@ -0,0 +1,407 @@ + + + + + + + Cluster Debug Interface - Coordinations + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading coordination data... +
+ +
+ Showing {{ filteredData.length }} of {{ coordinationData.length }} coordinations from {{ selectedHost }} +
+ +
+ No coordination data found for {{ selectedHost }} +
+ +
+
+
+
+
{{ coordination.txnId }}
+
ID: {{ coordination.coordinationId }}
+
+
{{ coordination.kind }}
+
+ + + + + + + + +
+
+ Description: + {{ coordination.description }} +
+
+ Nodes: + {{ coordination.nodes || 'N/A' }} +
+
+ Nodes In-flight: + {{ coordination.nodesInflight }} +
+
+ Nodes Contacted: + {{ coordination.nodesContacted }} +
+
+ Participants: + {{ coordination.participants || 'N/A' }} +
+
+ Replies: + {{ coordination.replies }} +
+
+ Tracker: + {{ coordination.tracker || 'N/A' }} +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/durability_service.html b/accord-cluster-debug/src/main/resources/web/durability_service.html new file mode 100644 index 0000000000..bc5bb7ff2b --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/durability_service.html @@ -0,0 +1,559 @@ + + + + + + + Cluster Debug Interface - Durability Service + + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading durability service data... +
+ +
+ No durability service data found for {{ selectedHost }} +
+ +
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+

+ + {{ entry.stopping ? 'STOPPING' : 'RUNNING' }} + + + {{ entry.stopped ? 'STOPPED' : 'ACTIVE' }} + +

+
+ +
+
+ Last Started: + {{ formatTimestamp(entry.lastStartedAt) }} +
+
+ Cycle Started: + {{ formatTimestamp(entry.cycleStartedAt) }} +
+
+ Retries: + {{ entry.retries }} +
+
+ Min: + {{ entry.min || 'N/A' }} +
+
+ Requested By: + {{ entry.requestedBy || 'N/A' }} +
+
+ Active: + {{ entry.active || 'N/A' }} +
+
+ Waiting: + {{ entry.waiting || 'N/A' }} +
+
+ Node Offset: + {{ entry.nodeOffset }} +
+
+ Cycle Offset: + {{ entry.cycleOffset }} +
+
+ Active Index: + {{ entry.activeIndex }} +
+
+ Next Index: + {{ entry.nextIndex }} +
+
+ Next To Index: + {{ entry.nextToIndex }} +
+
+ End Index: + {{ entry.endIndex }} +
+
+ Current Splits: + {{ entry.currentSplits }} +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/durable_before.html b/accord-cluster-debug/src/main/resources/web/durable_before.html new file mode 100644 index 0000000000..c8354e1740 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/durable_before.html @@ -0,0 +1,359 @@ + + + + + + + Cluster Debug Interface - Durable Before + + + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading durable before data... +
+ +
+ No durable before data found for {{ selectedHost }} +
+ +
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+
+ +
+
+ Quorum: + + + N/A +
+
+ Universal: + + + N/A +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/progress_log.html b/accord-cluster-debug/src/main/resources/web/progress_log.html new file mode 100644 index 0000000000..bc9dbccf89 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/progress_log.html @@ -0,0 +1,576 @@ + + + + + + + Cluster Debug Interface - Progress Log + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading progress log data... +
+ +
+ Showing {{ filteredData.length }} of {{ progressLogData.length }} progress log entries from {{ selectedHost }} +
+ +
+ No progress log data found for {{ selectedHost }} +
+ +
+
+ Command Store {{ commandStoreId }} +
+ +
+
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+
{{ entry.txnId }}
+
{{ entry.tableId }}
+
+
+ + {{ entry.contactEveryone ? 'Contact Everyone' : 'Selective Contact' }} + +
+
+ +
+ +
+
⏳ Waiting State
+
+ Initialised: + + {{ entry.waitingIsUninitialised ? 'No' : 'Yes' }} + +
+
+ Blocked Until: + {{ entry.waitingBlockedUntil }} +
+
+ Home Satisfies: + {{ entry.waitingHomeSatisfies }} +
+
+ Progress: + {{ entry.waitingProgress }} +
+
+ Retry Count: + {{ entry.waitingRetryCounter }} +
+
+ Key Tracker: + {{ entry.waitingPackedKeyTrackerBits }} +
+
+ Scheduled At: + {{ formatTimestampFromMillis(entry.waitingScheduledAt) }} +
+
+ + +
+
🏠 Home State
+
+ Phase: + {{ entry.homePhase }} +
+
+ Progress: + {{ entry.homeProgress }} +
+
+ Retry Count: + {{ entry.homeRetryCounter }} +
+
+ Scheduled At: + {{ formatTimestampFromMillis(entry.homeScheduledAt) }} +
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/redundant_before.html b/accord-cluster-debug/src/main/resources/web/redundant_before.html new file mode 100644 index 0000000000..1d57a21f74 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/redundant_before.html @@ -0,0 +1,556 @@ + + + + + + + Cluster Debug Interface - Redundant Before + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading redundant before data... +
+ +
+ No redundant before data found for {{ selectedHost }} +
+ +
+
+ Command Store {{ commandStoreId }} +
+ +
+
+
+ {{ keyspaceTable }} +
+ +
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+

Epochs: {{ entry.startEpoch }} → {{ entry.endEpoch }}

+
+ +
+
+ Table ID: + {{ entry.tableId }} +
+
+ GC Before: + {{ entry.gcBefore || 'N/A' }} +
+
+ Shard Applied: + {{ entry.shardApplied || 'N/A' }} +
+
+ Quorum Applied: + {{ entry.quorumApplied || 'N/A' }} +
+
+ Locally Applied: + {{ entry.locallyApplied || 'N/A' }} +
+
+ Locally Durable (Command Store): + {{ entry.locallyDurableToCommandStore || 'N/A' }} +
+
+ Locally Durable (Data Store): + {{ entry.locallyDurableToDataStore || 'N/A' }} +
+
+ Locally Redundant: + {{ entry.locallyRedundant || 'N/A' }} +
+
+ Locally Synced: + {{ entry.locallySynced || 'N/A' }} +
+
+ Locally Witnessed: + {{ entry.locallyWitnessed || 'N/A' }} +
+
+ Pre Bootstrap: + {{ entry.preBootstrap || 'N/A' }} +
+
+ Stale Until At Least: + {{ entry.staleUntilAtLeast || 'N/A' }} +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/topologies.html b/accord-cluster-debug/src/main/resources/web/topologies.html new file mode 100644 index 0000000000..48d14c3f60 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/topologies.html @@ -0,0 +1,469 @@ + + + + + + + Cluster Debug Interface - Topologies + + + + + +
+ + +
+ + +
+ +
+ {{ error }} +
+ +
+ +
+ +
+ Loading topology data... +
+ +
+ No topology data found for {{ selectedHost }} +
+ +
+
+
+ Epoch {{ topology.epoch ? topology.epoch.epoch : 'Unknown' }} + + Metadata:{{ topology.epoch.readyMetadata || 'N/A' }} | + Coordinate:{{ topology.epoch.readyCoordinate || 'N/A' }} | + Data:{{ topology.epoch.readyData || 'N/A' }} | + Reads:{{ topology.epoch.readyReads || 'N/A' }} + +
+
+ + {{ topology.epoch.ready ? 'READY' : 'NOT READY' }} + +
+
+ +
+
+ No table changes in this epoch +
+ +
+
+
+ {{ tableEpoch.keyspaceName }}.{{ tableEpoch.tableName }} +
+ +
+
+ Added: +
+ {{ node }} +
+
+ +
+ Removed: +
+ {{ node }} +
+
+ +
+ Synced/Closed: +
+ {{ node }} + {{ node }} +
+
+ +
+ Retired: +
+ {{ node }} +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/txn.html b/accord-cluster-debug/src/main/resources/web/txn.html new file mode 100644 index 0000000000..ad79719e33 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/txn.html @@ -0,0 +1,623 @@ + + + + + + + Transaction Search - Multi-Host Query + + + + +
+
+

Transaction Search

+
+ Searching for Transaction ID: +
{{ txnId }}
+
Querying all configured hosts for transaction state and blocking dependencies...
+
+
+ +
+ Searching across all hosts... +
+ +
+ {{ error }} +
+ +
+
+
{{ totalHosts }}
+
Total Hosts
+
+
+
{{ hostsWithData }}
+
Hosts with Data
+
+
+
{{ totalTransactions }}
+
Total Transactions
+
+
+
{{ hostsWithErrors }}
+
Hosts with Errors
+
+
+ +
+
+
+ {{ hostResult.hostname }} + + {{ hostResult.statusText }} + +
+ +
+
+ Querying host... +
+ +
+ {{ hostResult.errorMessage }} +
+ +
+
+
+ Store {{ transaction.commandStoreId }} + + {{ transaction.saveStatus || 'Unknown' }} + +
+ +
+
+ Route: + {{ transaction.route }} +
+
+ Durability: + {{ transaction.durability }} +
+
+ Execute At: + {{ transaction.executeAt }} +
+
+ Executes At Least: + {{ transaction.executesAtLeast }} +
+
+ Txn: + {{ transaction.txn }} +
+
+ Deps: + {{ transaction.deps }} +
+
+ Waiting On: + {{ transaction.waitingOn }} +
+
+ Writes: + {{ transaction.writes }} +
+
+ Result: + {{ transaction.result }} +
+
+ + +
+
+ 🚫 Transaction Blocked By ({{ transaction.blockedBy.length }} dependencies) +
+
+
+ {{ blocking.blockedBy }} + Depth {{ blocking.depth }} +
+
+
+ Reason: + {{ blocking.reason }} +
+
+ Keyspace: + {{ blocking.keyspaceName }} +
+
+ Table: + {{ blocking.tableName }} +
+
+ Key: + {{ blocking.key }} +
+
+ Blocking Status: + {{ blocking.saveStatus }} +
+
+ Blocking Execute At: + {{ blocking.executeAt }} +
+
+
+
+
+
+ +
+ No transaction data found +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/widgets/navigation.js b/accord-cluster-debug/src/main/resources/web/widgets/navigation.js new file mode 100644 index 0000000000..0a0136c695 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/widgets/navigation.js @@ -0,0 +1,102 @@ +// Vue Navigation Widget for Cassandra Accord Debug Interface +// Usage: + +(function() { + 'use strict'; + + // Define the Vue component + const NavigationWidget = { + name: 'NavigationWidget', + template: ` + + ` + }; + + // Make component available globally + if (typeof window !== 'undefined') { + window.NavigationWidgetComponent = NavigationWidget; + } + + // Add CSS styles + const style = document.createElement('style'); + style.textContent = ` + .navigation-widget { + margin: 1rem 0; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 5px; + border: 1px solid #dee2e6; + } + + .nav-menu { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + } + + .nav-link { + display: inline-block; + padding: 0.5rem 1rem; + color: #007bff; + text-decoration: none; + background-color: white; + border: 1px solid #007bff; + border-radius: 4px; + font-weight: 500; + transition: all 0.2s ease; + } + + .nav-link:hover { + background-color: #007bff; + color: white; + text-decoration: none; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2); + } + + .nav-link:active { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 123, 255, 0.2); + } + + @media (max-width: 768px) { + .nav-menu { + flex-direction: column; + align-items: stretch; + } + + .nav-link { + text-align: center; + } + } + `; + + if (document.head) { + document.head.appendChild(style); + } +})(); \ No newline at end of file diff --git a/accord-cluster-debug/src/main/resources/web/widgets/transaction_widget.js b/accord-cluster-debug/src/main/resources/web/widgets/transaction_widget.js new file mode 100644 index 0000000000..3cae479979 --- /dev/null +++ b/accord-cluster-debug/src/main/resources/web/widgets/transaction_widget.js @@ -0,0 +1,218 @@ +// Vue Transaction Widget for Cassandra Accord Debug Interface +// Usage: + +(function() { + 'use strict'; + + // Define the Vue component + const TransactionWidget = { + name: 'TransactionWidget', + props: { + txnId: { + type: String, + required: true + }, + type: { + type: String, + default: 'default' + } + }, + template: ` +
+
+ {{ txnId }} +
+
+ `, + methods: { + openTransactionSearch(txnId) { + const url = `/txn.html?txn_id=${encodeURIComponent(txnId)}`; + window.open(url, '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes'); + }, + + formatTimestamp(timestampStr) { + if (!timestampStr || timestampStr === 'N/A') { + return 'No timestamp available'; + } + + try { + // Parse timestamp format: [18,1756144112990011,130(KW),1] + const match = timestampStr.match(/\[(\d+),(\d+),(\d+).*\]/); + if (!match) { + return 'Invalid timestamp format'; + } + + const microTimestamp = parseInt(match[2]); + const millisTimestamp = Math.floor(microTimestamp / 1000); + const date = new Date(millisTimestamp); + + if (isNaN(date.getTime())) { + return 'Invalid timestamp'; + } + + // Format as DD-MM-YYYY HH-MM-SS.mmm + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const milliseconds = String(date.getMilliseconds()).padStart(3, '0'); + + return `${day}-${month}-${year} ${hours}-${minutes}-${seconds}.${milliseconds}`; + } catch (error) { + console.error('Error parsing timestamp:', timestampStr, error); + return 'Error parsing timestamp'; + } + }, + + formatTimestampWithRecency(timestampStr) { + const formattedDate = this.formatTimestamp(timestampStr); + if (formattedDate === 'No timestamp available' || formattedDate === 'Invalid timestamp format' || formattedDate === 'Error parsing timestamp') { + return formattedDate; + } + + const recency = this.getTimestampRecency(timestampStr); + if (recency) { + return `${formattedDate} (${recency})`; + } + return formattedDate; + }, + + getTimestampRecency(timestampStr) { + if (!timestampStr || timestampStr === 'N/A') { + return null; + } + + try { + const match = timestampStr.match(/\[(\d+),(\d+),(\d+).*\]/); + if (!match) { + return null; + } + + const microTimestamp = parseInt(match[2]); + const millisTimestamp = Math.floor(microTimestamp / 1000); + const timestampDate = new Date(millisTimestamp); + const now = new Date(); + + if (isNaN(timestampDate.getTime())) { + return null; + } + + const diffMinutes = Math.floor((now - timestampDate) / (1000 * 60)); + + if (diffMinutes < 1) { + return 'just now'; + } else if (diffMinutes === 1) { + return '1 minute ago'; + } else if (diffMinutes < 60) { + return `${diffMinutes} minutes ago`; + } else if (diffMinutes < 120) { + return '1 hour ago'; + } else if (diffMinutes < 1440) { + const hours = Math.floor(diffMinutes / 60); + return `${hours} hours ago`; + } else { + const days = Math.floor(diffMinutes / 1440); + return days === 1 ? '1 day ago' : `${days} days ago`; + } + } catch (error) { + return null; + } + }, + + getTimestampClass(timestampStr) { + if (!timestampStr || timestampStr === 'N/A') { + return ''; + } + + try { + const match = timestampStr.match(/\[(\d+),(\d+),(\d+).*\]/); + if (!match) { + return ''; + } + + const microTimestamp = parseInt(match[2]); + const millisTimestamp = Math.floor(microTimestamp / 1000); + const timestampDate = new Date(millisTimestamp); + const now = new Date(); + + if (isNaN(timestampDate.getTime())) { + return ''; + } + + const diffMinutes = Math.floor((now - timestampDate) / (1000 * 60)); + + if (diffMinutes <= 10) { + return 'recent'; // Green-ish background + } else if (diffMinutes <= 20) { + return 'moderate'; // Yellow background + } else { + return 'old'; // Red-ish background + } + } catch (error) { + return ''; + } + } + } + }; + + // Make component available globally + if (typeof window !== 'undefined') { + window.TransactionWidgetComponent = TransactionWidget; + } + + // Add CSS styles + const style = document.createElement('style'); + style.textContent = ` + .transaction-widget { + display: inline-block; + } + + .transaction-widget .txn-id { + font-family: monospace; + color: #007bff; + font-weight: 500; + cursor: pointer; + text-decoration: underline dotted; + transition: color 0.2s; + padding: 0.25rem 0.5rem; + border-radius: 3px; + } + + .transaction-widget .txn-id:hover { + color: #0056b3; + text-decoration: underline solid; + } + + .transaction-widget .txn-id.recent { + background-color: #d4edda; + color: #155724; + } + + .transaction-widget .txn-id.moderate { + background-color: #fff3cd; + color: #856404; + } + + .transaction-widget .txn-id.old { + background-color: #f8d7da; + color: #721c24; + } + + .transaction-widget--quorum .txn-id { + border-left: 3px solid #fd7e14; + } + + .transaction-widget--universal .txn-id { + border-left: 3px solid #6f42c1; + } + `; + + if (document.head) { + document.head.appendChild(style); + } +})(); \ No newline at end of file diff --git a/accord-core/CLAUDE.md b/accord-core/CLAUDE.md new file mode 100644 index 0000000000..63bdd5239d --- /dev/null +++ b/accord-core/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +This is a Gradle-based Java project. Key commands: + +- **Build**: `./gradlew build` +- **Test**: `./gradlew test` +- **Install locally**: `./gradlew install` (or `./gradlew publishToMavenLocal`) +- **Burn testing**: `./gradlew burn` (stress testing with 1 cluster) +- **Burn loop**: `./gradlew burnloop` (continuous burn testing, configurable with `-PburnTimes=N`) + +## Core Architecture + +This is **Apache Cassandra Accord**, a general-purpose transactions library implementing a leaderless consensus protocol for highly available transactions. + +### Key Components + +1. **Node** (`accord.local.Node`): Central coordination point that manages transaction lifecycle, command stores, and cluster communication. + +2. **Command/Transaction Flow**: + - **Txn** (`accord.primitives.Txn`): Transaction definition with Read/Write/EphemeralRead kinds + - **Command** (`accord.local.Command`): Local representation of transactions with status tracking + - **TxnId** (`accord.primitives.TxnId`): Globally unique transaction identifiers + +3. **Coordination** (`accord.coordinate.*`): + - **CoordinateTransaction**: Orchestrates transaction execution across cluster + - **PreAccept/Accept**: Two-phase consensus protocol messages + - Various tracking classes for managing quorums and responses + +4. **Storage & State**: + - **CommandStore** (`accord.local.CommandStore`): Per-shard transaction storage + - **SafeCommandStore**: Thread-safe wrapper for command operations + - **CommandsForKey** (CFK): Efficient key-based command indexing + +5. **Messaging** (`accord.messages.*`): Protocol messages for cluster communication including PreAccept, Accept, Apply, ReadData, etc. + +6. **Primitives** (`accord.primitives.*`): + - **Route/Keys/Ranges**: Data locality and routing + - **Deps**: Transaction dependency tracking + - **Status/SaveStatus**: Transaction state management + - **Ballot**: Consensus voting + +7. **Topology** (`accord.topology.*`): Cluster membership and shard management + +8. **Utils** (`accord.utils.*`): Specialized data structures including BTree implementations, async utilities, and custom collections + +### Architecture Notes + +- Uses leaderless consensus (no single coordinator) +- Implements dependency tracking for conflict resolution +- Supports both synchronous and asynchronous operations via AsyncChain +- Built for high availability with configurable durability levels +- Custom serialization and memory-efficient data structures throughout \ No newline at end of file diff --git a/accord-core/build.gradle b/accord-core/build.gradle index b30737c17f..e1a6891e5f 100644 --- a/accord-core/build.gradle +++ b/accord-core/build.gradle @@ -53,6 +53,7 @@ dependencies { exclude group: 'org.slf4j', module: 'jcl-over-slf4j' } testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' + testImplementation project(':accord-debug') } task burn(type: JavaExec) { diff --git a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java index d7438d6990..834e8f63c7 100644 --- a/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java +++ b/accord-core/src/main/java/accord/impl/InMemoryCommandStore.java @@ -212,7 +212,7 @@ protected boolean canExposeUnloaded() @VisibleForTesting public NavigableMap unsafeCommands() { - return commands; + return new TreeMap<>(commands); } @VisibleForTesting @@ -326,6 +326,8 @@ public GlobalCommandsForKey commandsForKey(RoutingKey key) return commandsForKey.computeIfAbsent(key, GlobalCommandsForKey::new); } + public abstract GlobalCommandsForKey commandsForKey(String key); + public boolean hasCommandsForKey(RoutingKey key) { return commandsForKey.containsKey(key); @@ -941,6 +943,11 @@ public Synchronized(int id, NodeCommandStoreService time, Agent agent, DataStore super(id, time, agent, store, progressLogFactory, listenersFactory, epochUpdateHolder, journal); } + public GlobalCommandsForKey commandsForKey(String key) + { + throw new UnsupportedOperationException(); + } + private synchronized void maybeRun() { if (active != null) @@ -1036,6 +1043,12 @@ public SingleThread(int id, NodeCommandStoreService time, Agent agent, DataStore executor.execute(() -> thread = Thread.currentThread()); } + public GlobalCommandsForKey commandsForKey(String key) + { + throw new UnsupportedOperationException(); + + } + void assertThread() { Thread current = Thread.currentThread(); diff --git a/accord-core/src/main/java/accord/local/Command.java b/accord-core/src/main/java/accord/local/Command.java index 8bce357d1b..e73452056c 100644 --- a/accord-core/src/main/java/accord/local/Command.java +++ b/accord-core/src/main/java/accord/local/Command.java @@ -18,6 +18,7 @@ package accord.local; +import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -913,6 +914,20 @@ public TxnId txnId(int i) throw new IndexOutOfBoundsException(i + " >= " + txnIdCount()); } + public List asListUnsafe() + { + return new AbstractList<>() + { + @Override + public TxnId get(int index) { return txnId(index); } + @Override + public int size() + { + return txnIdCount(); + } + }; + } + int indexOf(TxnId txnId) { if (txnId.domain() == Range) diff --git a/accord-core/src/main/java/accord/local/RedundantBefore.java b/accord-core/src/main/java/accord/local/RedundantBefore.java index 078d58953a..108240057c 100644 --- a/accord-core/src/main/java/accord/local/RedundantBefore.java +++ b/accord-core/src/main/java/accord/local/RedundantBefore.java @@ -18,27 +18,11 @@ package accord.local; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import accord.api.RoutingKey; import accord.api.VisibleForImplementation; import accord.local.RedundantStatus.Coverage; -import accord.local.RedundantStatus.SomeStatus; import accord.local.RedundantStatus.Property; +import accord.local.RedundantStatus.SomeStatus; import accord.primitives.AbstractRanges; import accord.primitives.Deps; import accord.primitives.EpochSupplier; @@ -54,10 +38,26 @@ import accord.utils.ReducingIntervalMap; import accord.utils.ReducingRangeMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import static accord.api.ProtocolModifiers.Toggles.requiresUniqueHlcs; import static accord.local.RedundantStatus.Coverage.SOME; -import static accord.local.RedundantStatus.ONLY_LE_MASK; import static accord.local.RedundantStatus.NOT_OWNED_ONLY; +import static accord.local.RedundantStatus.ONLY_LE_MASK; import static accord.local.RedundantStatus.PRE_BOOTSTRAP_OR_STALE_ONLY; import static accord.local.RedundantStatus.Property.GC_BEFORE; import static accord.local.RedundantStatus.Property.LOCALLY_APPLIED; @@ -70,9 +70,9 @@ import static accord.local.RedundantStatus.Property.PRE_BOOTSTRAP; import static accord.local.RedundantStatus.Property.PRE_BOOTSTRAP_OR_STALE; import static accord.local.RedundantStatus.Property.SHARD_APPLIED; -import static accord.local.RedundantStatus.WAS_OWNED_SYNCED; import static accord.local.RedundantStatus.WAS_OWNED_ONLY; import static accord.local.RedundantStatus.WAS_OWNED_RETIRED; +import static accord.local.RedundantStatus.WAS_OWNED_SYNCED; import static accord.local.RedundantStatus.addHistory; import static accord.local.RedundantStatus.any; import static accord.local.RedundantStatus.mask; @@ -88,6 +88,60 @@ public class RedundantBefore extends ReducingRangeMap { + /** + * Creates a detailed visualization of redundantBefore showing max bounds for every property in each range. + * This is useful for debugging redundantBefore state by showing which transactions are redundant for each property + * across different ranges. + * + * @param redundantBefore the RedundantBefore instance to visualize + * @return a formatted string showing ranges and their property max bounds + */ + public static String print(RedundantBefore redundantBefore) + { + if (redundantBefore == null || redundantBefore.size() == 0) + return "RedundantBefore{EMPTY}"; + + StringBuilder builder = new StringBuilder("RedundantBefore{\n"); + + redundantBefore.foldl((bounds, sb, p1, p2) -> { + if (bounds != null) { + sb.append(" Range[").append(bounds.range).append("] {\n"); + sb.append(" Epochs: ").append(bounds.startEpoch).append(" to ").append(bounds.endEpoch).append('\n'); + + if (bounds.staleUntilAtLeast != null) { + sb.append(" StaleUntilAtLeast: ").append(bounds.staleUntilAtLeast).append('\n'); + } + + sb.append(" PropertyMaxBounds:\n"); + for (RedundantStatus.Property property : RedundantStatus.Property.values()) + { + TxnId maxBound = bounds.maxBound(property); + if (maxBound != TxnId.NONE) + { + sb.append(" ").append(property).append(": ").append(maxBound).append('\n'); + } + } + + sb.append(" BootstrappedAt: ").append(bounds.bootstrappedAt).append('\n'); + sb.append(" GcBefore: ").append(bounds.gcBefore).append('\n'); + sb.append(" }\n"); + } + return sb; + }, builder, null, null, ignore -> false); + + builder.append("}"); + return builder.toString(); + } + + public static boolean satisfies(RedundantBefore redundantBefore, TxnId txnId, RoutingKey routingKey, RedundantStatus.Property property) + { + if (redundantBefore == null || redundantBefore.size() == 0) + return false; + + Bounds bounds = redundantBefore.get(routingKey); + return bounds != null && bounds.is(txnId, property); + } + public interface RedundantBeforeSupplier { RedundantBefore redundantBefore(); diff --git a/accord-core/src/main/java/accord/local/durability/ShardDurability.java b/accord-core/src/main/java/accord/local/durability/ShardDurability.java index a4604f617c..b5fdf5bd53 100644 --- a/accord-core/src/main/java/accord/local/durability/ShardDurability.java +++ b/accord-core/src/main/java/accord/local/durability/ShardDurability.java @@ -94,6 +94,15 @@ public Waiting next() { return next; } + + public String toString() + { + return "Waiting{" + + "request=" + request + + ", ranges=" + ranges + + ", next=" + next + + '}'; + } } // TODO (expected): support intra-shard parallelism diff --git a/accord-core/src/main/java/accord/primitives/Ballot.java b/accord-core/src/main/java/accord/primitives/Ballot.java index 9bfc787a0f..bf62b899a6 100644 --- a/accord-core/src/main/java/accord/primitives/Ballot.java +++ b/accord-core/src/main/java/accord/primitives/Ballot.java @@ -43,8 +43,20 @@ public static Ballot fromValues(long epoch, long hlc, int flags, Id node) return new Ballot(epoch, hlc, flags, node); } - public static final Ballot ZERO = new Ballot(Timestamp.NONE); - public static final Ballot MAX = new Ballot(Timestamp.MAX); + public static final Ballot ZERO = new Ballot(Timestamp.NONE) { + @Override + public String toStandardString() + { + return "NONE"; + } + }; + public static final Ballot MAX = new Ballot(Timestamp.MAX) { + @Override + public String toStandardString() + { + return "ZERO"; + } + }; public Ballot(Timestamp from) { diff --git a/accord-core/src/main/java/accord/primitives/PartialDeps.java b/accord-core/src/main/java/accord/primitives/PartialDeps.java index 75c1ce3739..652519e0cc 100644 --- a/accord-core/src/main/java/accord/primitives/PartialDeps.java +++ b/accord-core/src/main/java/accord/primitives/PartialDeps.java @@ -18,6 +18,8 @@ package accord.primitives; +import java.util.AbstractList; +import java.util.List; import java.util.Objects; import accord.api.RoutingKey; @@ -118,6 +120,20 @@ public Deps asFullUnsafe() return new Deps(keyDeps, rangeDeps); } + public List asListUnsafe() + { + return new AbstractList<>() + { + @Override + public TxnId get(int index) { return txnId(index); } + @Override + public int size() + { + return txnIdCount(); + } + }; + } + public Deps reconstitute(FullRoute route) { if (!covers(route.participants())) diff --git a/accord-core/src/test/java/accord/burn/BurnTestBase.java b/accord-core/src/test/java/accord/burn/BurnTestBase.java index a88c2b52bc..3bb42279c6 100644 --- a/accord-core/src/test/java/accord/burn/BurnTestBase.java +++ b/accord-core/src/test/java/accord/burn/BurnTestBase.java @@ -642,7 +642,7 @@ protected static Verifier createVerifier(int keyCount) protected static void run(long seed) { - Duration timeout = Duration.ofMinutes(3); + Duration timeout = Duration.ofMinutes(3000); try { TimeoutUtils.runBlocking(timeout, "BurnTest with timeout", () -> run(seed, 1000)); diff --git a/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java b/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java index e6404ef94f..84ed5925e7 100644 --- a/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java +++ b/accord-core/src/test/java/accord/impl/PrefixedIntHashKey.java @@ -196,6 +196,34 @@ public Hash(int prefix, int hash) super(prefix, hash); } + /** + * Parse a Hash from string format "prefix#hash" + * @param str String in format "prefix#hash", e.g. "0#32960" + * @return Hash instance + * @throws IllegalArgumentException if string format is invalid + */ + public static Hash fromString(String str) + { + if (str == null || str.trim().isEmpty()) + throw new IllegalArgumentException("String cannot be null or empty"); + + String trimmed = str.trim(); + int hashIndex = trimmed.indexOf('#'); + if (hashIndex == -1) + throw new IllegalArgumentException("Invalid format: expected 'prefix#hash', got: " + str); + + try + { + int prefix = Integer.parseInt(trimmed.substring(0, hashIndex)); + int hash = Integer.parseInt(trimmed.substring(hashIndex + 1)); + return new Hash(prefix, hash); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("Invalid number format in: " + str, e); + } + } + @Override public accord.primitives.Range asRange() { @@ -216,6 +244,12 @@ public Object suffix() { return hash; } + + @Override + public String toString() + { + return prefix + "#" + hash; + } } public static class Range extends accord.primitives.Range.EndInclusive diff --git a/accord-core/src/test/java/accord/impl/basic/Cluster.java b/accord-core/src/test/java/accord/impl/basic/Cluster.java index ea65c48871..81b3d7cf96 100644 --- a/accord-core/src/test/java/accord/impl/basic/Cluster.java +++ b/accord-core/src/test/java/accord/impl/basic/Cluster.java @@ -48,6 +48,8 @@ import java.util.stream.Stream; import javax.annotation.Nullable; +import accord.debug.NewServer; + import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -633,6 +635,7 @@ public static Map run(Id[] nodes, int[] prefixes, MessageLis Map nodeMap = new LinkedHashMap<>(); Map executorMap = new LinkedHashMap<>(); Map journalMap = new LinkedHashMap<>(); + try { RandomSource random = randomSupplier.get(); @@ -679,6 +682,7 @@ public static Map run(Id[] nodes, int[] prefixes, MessageLis TopologyRandomizer configRandomizer = new TopologyRandomizer(randomSupplier, prefixes, topology, topologyUpdates, nodeMap::get, schemaApply); List durabilityServices = new ArrayList<>(); List services = new ArrayList<>(); + NewServer debugServer = new NewServer(8080); for (Id id : nodes) { ClusterScheduler scheduler = sinks.new ClusterScheduler(id.id); @@ -702,6 +706,15 @@ public static Map run(Id[] nodes, int[] prefixes, MessageLis durabilityServices.add(node.durability()); nodeMap.put(id, node); durabilityServices.add(new DurabilityService(node)); + + debugServer.registerNode(node); + } + debugServer.start(); + + for (Node node : nodeMap.values()) + { + node.configService().registerListener((ListStore) node.commandStores().dataStore()); + node.configService().registerListener(node.durability()); } for (Node node : nodeMap.values()) @@ -763,9 +776,12 @@ public static Map run(Id[] nodes, int[] prefixes, MessageLis Scheduled restart = clusterScheduler.recurring(() -> { Id id = pickNodeNotBootstrapping(random, nodesList, nodeMap); + System.out.println("id = " + id); + NewServer.getInstance().pause(); if (id == null) return; + CommandStores stores = nodeMap.get(id).commandStores(); while (sinks.drain(getPendingPredicate(id, stores.all()))) ; @@ -811,7 +827,7 @@ public static Map run(Id[] nodes, int[] prefixes, MessageLis // we can get ahead of prior state by executing further if we skip some earlier phase's dependencies listStore.checkAtLeast(stores, prevData); trace.debug("Done with replay."); - }, () -> random.nextInt(10, 30), SECONDS); + }, () -> random.nextInt(3, 5), SECONDS); durabilityServices.forEach(DurabilityService::start); services.forEach(Service::start); diff --git a/accord-core/src/test/java/accord/impl/basic/Cluster.java.backup b/accord-core/src/test/java/accord/impl/basic/Cluster.java.backup new file mode 100644 index 0000000000..0ef9d57529 --- /dev/null +++ b/accord-core/src/test/java/accord/impl/basic/Cluster.java.backup @@ -0,0 +1,1443 @@ +/* + * 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 accord.impl.basic; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.api.Agent; +import accord.api.AsyncExecutor; +import accord.api.Journal; +import accord.api.MessageSink; +import accord.api.RoutingKey; +import accord.api.Scheduler; +import accord.api.Scheduler.Scheduled; +import accord.burn.BurnTestConfigurationService; +import accord.burn.TopologyUpdates; +import accord.burn.random.FrequentLargeRange; +import accord.coordinate.CoordinationAdapter; +import accord.impl.DefaultLocalListeners; +import accord.impl.DefaultRemoteListeners; +import accord.impl.DefaultTimeouts; +import accord.impl.InMemoryCommandStore.GlobalCommand; +import accord.impl.MessageListener; +import accord.impl.PrefixedIntHashKey; +import accord.impl.SizeOfIntersectionSorter; +import accord.impl.TopologyFactory; +import accord.impl.basic.DelayedCommandStores.DelayedCommandStore; +import accord.impl.list.ListAgent; +import accord.impl.list.ListStore; +import accord.impl.progresslog.DefaultProgressLogs; +import accord.local.Cleanup; +import accord.local.Command; +import accord.local.CommandStore; +import accord.local.CommandStores; +import accord.local.Node; +import accord.local.Node.Id; +import accord.local.RedundantBefore; +import accord.local.ShardDistributor; +import accord.local.StoreParticipants; +import accord.local.TimeService; +import accord.local.UniqueTimeService.AtomicUniqueTimeWithStaleReservation; +import accord.local.cfk.CommandsForKey; +import accord.local.cfk.Serialize; +import accord.local.durability.DurabilityService; +import accord.messages.Message; +import accord.messages.MessageType; +import accord.messages.Reply; +import accord.messages.Request; +import accord.messages.SafeCallback; +import accord.primitives.Range; +import accord.primitives.Ranges; +import accord.primitives.RoutableKey; +import accord.primitives.SaveStatus; +import accord.primitives.Status; +import accord.primitives.Timestamp; +import accord.primitives.Txn; +import accord.primitives.TxnId; +import accord.topology.Topology; +import accord.topology.TopologyRandomizer; +import accord.utils.Gens; +import accord.utils.Invariants; +import accord.utils.LazyToString; +import accord.utils.RandomSource; +import accord.utils.ReflectionUtils; +import accord.utils.Timestamped; +import accord.utils.UnhandledEnum; +import accord.utils.async.AsyncChains; +import accord.utils.async.AsyncResult; +import org.agrona.collections.Int2ObjectHashMap; + +import static accord.impl.basic.Cluster.OverrideLinksKind.NONE; +import static accord.impl.basic.Cluster.OverrideLinksKind.RANDOM_BIDIRECTIONAL; +import static accord.impl.basic.NodeSink.Action.DELIVER; +import static accord.impl.basic.NodeSink.Action.DROP; +import static accord.local.Cleanup.EXPUNGE; +import static accord.local.Cleanup.INVALIDATE; +import static accord.local.Cleanup.Input.FULL; +import static accord.local.Command.NotDefined.uninitialised; +import static accord.local.StoreParticipants.Filter.LOAD; +import static accord.utils.Invariants.Paranoia.LINEAR; +import static accord.utils.Invariants.ParanoiaCostFactor.HIGH; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; + +public class Cluster +{ + public static final Logger trace = LoggerFactory.getLogger("accord.impl.basic.Trace"); + + public static class Stats + { + final Object key; + int count; + + public Stats(Object key) + { + this.key = key; + } + + public int count() { return count; } + public String toString() { return key + ": " + count; } + } + + public static class LinkConfig + { + final Function, BiFunction> overrideLinks; + final BiFunction defaultLinks; + + public LinkConfig(Function, BiFunction> overrideLinks, BiFunction defaultLinks) + { + this.overrideLinks = overrideLinks; + this.defaultLinks = defaultLinks; + } + } + + static class Link + { + final Supplier action; + final LongSupplier latencyMicros; + + Link(Supplier action, LongSupplier latencyMicros) + { + this.action = action; + this.latencyMicros = latencyMicros; + } + } + + public class ClusterScheduler implements Scheduler + { + final int source; + + ClusterScheduler(int source) + { + this.source = source; + } + + @Override + public void now(Runnable run) + { + run.run(); + } + + @Override + public Scheduled recurring(Runnable run, long delay, TimeUnit units) + { + return recurring(run, () -> delay, units); + } + + @Override + public Scheduled once(Runnable run, long delay, TimeUnit units) + { + RecurringPendingRunnable result = new RecurringPendingRunnable(source, null, run, () -> delay, units, false); + pending.add(result, delay, units); + return result; + } + + @Override + public Scheduled selfRecurring(Runnable run, long delay, TimeUnit units) + { + RecurringPendingRunnable result = new RecurringPendingRunnable(source, null, run, () -> delay, units, true); + pending.add(result, delay, units); + return result; + } + + public Scheduled recurring(Runnable run, LongSupplier delay, TimeUnit units) + { + RecurringPendingRunnable result = new RecurringPendingRunnable(source, pending, run, delay, units, true); + pending.add(result, delay.getAsLong(), units); + return result; + } + + public void onDone(Runnable run) + { + Cluster.this.onDone(run); + } + } + + final Map statsMap = new HashMap<>(); + + final RandomSource random; + final LinkConfig linkConfig; + final Function lookup; + final Function journalLookup; + final PendingQueue pending; + final Runnable checkFailures; + final List onDone = new ArrayList<>(); + final Consumer responseSink; + final Map sinks = new HashMap<>(); + final MessageListener messageListener; + int clock; + BiFunction links; + + public Cluster(RandomSource random, MessageListener messageListener, Supplier queueSupplier, Runnable checkFailures, Function lookup, Function journalLookup, IntSupplier rf, Consumer responseSink) + { + this.random = random; + this.messageListener = messageListener; + this.pending = queueSupplier.get(); + this.checkFailures = checkFailures; + this.lookup = lookup; + this.journalLookup = journalLookup; + this.responseSink = responseSink; + this.linkConfig = defaultLinkConfig(random, rf); + this.links = linkConfig.defaultLinks; + } + + NodeSink create(Id self, NodeSink.TimeoutSupplier timeouts) + { + NodeSink sink = new NodeSink(self, lookup, this, timeouts); + sinks.put(self, sink); + return sink; + } + + void add(Packet packet, long delay, TimeUnit unit) + { + MessageType type = packet.message.type(); + if (type != null) + statsMap.computeIfAbsent(type, Stats::new).count++; + if (trace.isTraceEnabled()) + trace.trace("{} {} {}", clock++, packet.message instanceof Reply ? "RPLY" : "SEND", packet); + if (lookup.apply(packet.dst) == null) responseSink.accept(packet); + else pending.add(packet, delay, unit); + + } + + public void processAll() + { + List pending = new ArrayList<>(); + { + // TODO (expected): this doesn't actually process all pending, as any queued tasks on executors aren't processed. + // should we perhaps queue them and then process them? + Pending next; + while (null != (next = this.pending.poll())) + pending.add(next); + } + + for (Pending next : pending) + { + Pending.Global.setActiveOrigin(next); + processNext(next); + Pending.Global.clearActiveOrigin(); + checkFailures.run(); + } + } + + boolean hasNonRecurring() + { + if (pending.hasNonRecurring()) + return true; + + for (Pending p : pending) + { + if (!(p instanceof RecurringPendingRunnable)) + continue; + + RecurringPendingRunnable r = (RecurringPendingRunnable) p.origin(); + if (r.requeue != null && r.requeue.hasNonRecurring()) + return true; + } + + return false; + } + + public boolean processPending() + { + checkFailures.run(); + // All remaining tasks are recurring + if (!hasNonRecurring()) + return false; + + Pending next = pending.poll(); + if (next == null) + return false; + + Pending.Global.setActiveOrigin(next); + processNext(next); + Pending.Global.clearActiveOrigin(); + + checkFailures.run(); + return true; + } + + /** + * Drain tasks that match predicate. + * + * Returns whether any tasks were processed + */ + public boolean drain(Predicate process) + { + List pending = this.pending.drain(process); + for (Pending p : pending) + processNext(p); + return !pending.isEmpty(); + } + + private void processNext(Object next) + { + if (next instanceof Packet) + { + Packet deliver = (Packet) next; + Node on = lookup.apply(deliver.dst); + + if (trace.isTraceEnabled()) + trace.trace("{} RECV[{}] {}", clock++, on.epoch(), deliver); + + if (deliver.message instanceof Reply) + { + Reply reply = (Reply) deliver.message; + SafeCallback callback = reply.isFinal() + ? sinks.get(deliver.dst).callbacks.remove(deliver.replyId) + : sinks.get(deliver.dst).callbacks.get(deliver.replyId); + + if (callback != null) + { + if (reply instanceof Reply.FailureReply) callback.failure(deliver.src, ((Reply.FailureReply) reply).failure); + else callback.success(deliver.src, reply); + } + } + + else on.receive((Request) deliver.message, deliver.src, deliver); + } + else + { + ((Runnable) next).run(); + } + } + + public void notifyDropped(Node.Id from, Node.Id to, long id, Message message) + { + if (trace.isTraceEnabled()) + trace.trace("{} DROP[{}] (from:{}, to:{}, {}:{}, body:{})", clock++, lookup.apply(to).epoch(), from, to, message instanceof Reply ? "replyTo" : "id", id, message); + } + + public void onDone(Runnable run) + { + onDone.add(run); + } + + // TODO (testing): merge with BurnTest.burn + public static Map run(Supplier randomSupplier, + int[] prefixes, + List nodes, + Topology initialTopology, + Function, Request> init) + { + List failures = Collections.synchronizedList(new ArrayList<>()); + MonitoredPendingQueue queue = new MonitoredPendingQueue(failures, new RandomDelayQueue(randomSupplier.get())); + Consumer retryBootstrap; + { + RandomSource rnd = randomSupplier.get(); + retryBootstrap = retry -> { + long delay = rnd.nextInt(1, 15); + queue.add(PendingRunnable.create(retry::run), delay, TimeUnit.SECONDS); + }; + } + IntSupplier coordinationDelays, progressDelays, timeoutDelays; + { + RandomSource rnd = randomSupplier.get(); + timeoutDelays = progressDelays = coordinationDelays = () -> rnd.nextInt(100, 1000); + } + RandomSource nowRandom = randomSupplier.get(); + Supplier nowSupplier = () -> { + RandomSource forked = nowRandom.fork(); + // TODO (testing): meta-randomise scale of clock drift + return FrequentLargeRange.builder(forked) + .ratio(1, 5) + .small(50, 5000, TimeUnit.MICROSECONDS) + .large(1, 10, TimeUnit.MILLISECONDS) + .build() + .mapAsLong(j -> Math.max(0, queue.nowInMillis() + TimeUnit.NANOSECONDS.toMillis(j))) + .asLongSupplier(forked); + }; + Supplier timeServiceSupplier = () -> TimeService.ofNonMonotonic(nowSupplier.get(), MILLISECONDS); + BiFunction, NodeSink.TimeoutSupplier, Agent> agentSupplier = (onStale, timeoutSupplier) -> new ListAgent(randomSupplier.get(), 1000L, failures::add, retryBootstrap, onStale, coordinationDelays, progressDelays, timeoutDelays, queue::nowInMillis, timeServiceSupplier.get(), timeoutSupplier); + SimulatedDelayedExecutorService globalExecutor = new SimulatedDelayedExecutorService(queue, new ListAgent(randomSupplier.get(), 1000L, failures::add, retryBootstrap, (i1, i2) -> { + throw new IllegalAccessError("Global executor should never get a stale event"); + }, () -> { throw new UnsupportedOperationException(); }, () -> { throw new UnsupportedOperationException(); }, timeoutDelays, queue::nowInMillis, timeServiceSupplier.get(), null), null); + TopologyFactory topologyFactory = new TopologyFactory(initialTopology.maxRf(), initialTopology.ranges().stream().toArray(Range[]::new)) + { + @Override + public Topology toTopology(Node.Id[] cluster) + { + return initialTopology; + } + }; + AtomicInteger counter = new AtomicInteger(); + AtomicReference> nodeMap = new AtomicReference<>(); + Map stats = Cluster.run(nodes.toArray(Node.Id[]::new), + prefixes, + MessageListener.get(), + () -> queue, + (id) -> globalExecutor, + agentSupplier, + queue::checkFailures, + ignore -> {}, + randomSupplier, + timeServiceSupplier, + topologyFactory, + new Supplier<>() + { + private Iterator requestIterator = null; + private final RandomSource rs = randomSupplier.get(); + @Override + public Packet get() + { + if (requestIterator == null) + { + Map nodes = nodeMap.get(); + requestIterator = Collections.singleton(init.apply(nodes)).iterator(); + } + if (!requestIterator.hasNext()) + return null; + Node.Id id = rs.pick(nodes); + return new Packet(id, id, Long.MAX_VALUE, counter.incrementAndGet(), requestIterator.next(), true); + } + }, + Runnable::run, + nodeMap::set, + InMemoryJournal::new); + if (!failures.isEmpty()) + { + AssertionError error = new AssertionError("Unexpected errors detected"); + failures.forEach(error::addSuppressed); + throw error; + } + return stats; + } + + static class RandomLoader + { + private final BooleanSupplier cacheEmptyChance; + private final BooleanSupplier cacheFullChance; + private final BooleanSupplier commandLoadedChance; + private final BooleanSupplier cfkLoadedChance; + private final BooleanSupplier tfkLoadedChance; + + final BooleanSupplier cmdCheckChance; + final BooleanSupplier cfkCheckChance; + static int cmdCounter, cfkCounter; + + RandomLoader(RandomSource random) + { + this(random.nextBoolean() ? 1.0f : random.nextFloat(), random); + } + + RandomLoader(float presentChance, RandomSource random) + { + this(Gens.supplier(Gens.bools().mixedDistribution().next(random), random), + Gens.supplier(Gens.bools().mixedDistribution().next(random), random), + random.biasedUniformBools(presentChance), + random.biasedUniformBools(presentChance), + random.biasedUniformBools(presentChance), + Invariants.testParanoia(LINEAR, LINEAR, HIGH) ? Gens.supplier(Gens.bools().mixedDistribution().next(random), random) : () -> random.decide(0.001f), + () -> random.decide(0.1f) + ); + } + + RandomLoader(BooleanSupplier cacheEmptyChance, BooleanSupplier cacheFullChance, + BooleanSupplier commandLoadedChance, BooleanSupplier cfkLoadedChance, BooleanSupplier tfkLoadedChance, + BooleanSupplier cmdCheckChance, BooleanSupplier cfkCheckChance) + { + this.cacheEmptyChance = cacheEmptyChance; + this.cacheFullChance = cacheFullChance; + this.commandLoadedChance = commandLoadedChance; + this.cfkLoadedChance = cfkLoadedChance; + this.tfkLoadedChance = tfkLoadedChance; + this.cmdCheckChance = cmdCheckChance; + this.cfkCheckChance = cfkCheckChance; + } + + public boolean cacheEmpty() { return cacheEmptyChance.getAsBoolean();} + public boolean cacheFull() { return cacheFullChance.getAsBoolean(); } + public boolean commandLoaded() { return commandLoadedChance.getAsBoolean(); } + public boolean cfkLoaded() { return cfkLoadedChance.getAsBoolean(); } + public boolean tfkLoaded() { return tfkLoadedChance.getAsBoolean(); } + + DelayedCommandStores.CacheLoading newLoader(Journal journal) + { + return new DelayedCommandStores.CacheLoading() + { + @Override + public boolean cacheEmpty() + { + return cacheEmptyChance.getAsBoolean(); + } + + @Override + public boolean cacheFull() + { + return cacheFullChance.getAsBoolean(); + } + + @Override + public boolean isLoaded(TxnId txnId) + { + return commandLoadedChance.getAsBoolean(); + } + + @Override + public boolean isLoaded(RoutingKey key) + { + return cfkLoadedChance.getAsBoolean(); + } + + @Override + public boolean tfkLoaded() + { + return tfkLoadedChance.getAsBoolean(); + } + + @Override + public void validateRead(CommandStore commandStore, Command command) + { + validate(commandStore, command, false); + } + + @Override + public void validateWrite(CommandStore commandStore, Command command) + { + validate(commandStore, command, true); + } + + public void validate(CommandStore commandStore, Command command, boolean isWrite) + { + if (command.txnId().kind() == Txn.Kind.EphemeralRead + || command.saveStatus() == SaveStatus.Uninitialised + || command.saveStatus() == SaveStatus.Vestigial + || command.saveStatus() == SaveStatus.Erased) + return; + + if (!cmdCheckChance.getAsBoolean()) + return; + + ++cmdCounter; + command = command.updateParticipants(command.participants().filter(LOAD, commandStore.unsafeGetRedundantBefore(), command.txnId(), command.executeAtIfKnown())); + // Journal will not have result persisted. This part is here for test purposes and ensuring that we have strict object equality. + Command reconstructed = journal.loadCommand(commandStore.id(), command.txnId(), commandStore.unsafeGetRedundantBefore(), commandStore.durableBefore()); + if (reconstructed == null || reconstructed.saveStatus().hasBeen(Status.Truncated)) + return; + + List> diff = ReflectionUtils.recursiveEquals(command, reconstructed); + if (!diff.isEmpty() && command.saveStatus().compareTo(SaveStatus.Erased) >= 0) + diff.removeIf(v -> v.path.equals(".participants.")); + Invariants.require(diff.isEmpty(), "Commands did not match: expected %s, given %s on %s, diff %s", command, reconstructed, commandStore, new LazyToString(() -> String.join("\n", Iterables.transform(diff, Object::toString)))); + } + + @Override + public void validateRead(CommandStore commandStore, CommandsForKey cfk) + { + if (cfk == null) return; + if (cfk.isLoadingPruned()) return; + + if (!cfkCheckChance.getAsBoolean()) + return; + + ++cfkCounter; + cfk = cfk.maximalPrune(); + ByteBuffer encoded = Serialize.toBytesWithoutKey(cfk); + CommandsForKey decoded = Serialize.fromBytes(cfk.key(), encoded); + Invariants.require(cfk.equalContents(decoded)); + } + }; + } + + } + + public static Map run(Id[] nodes, int[] prefixes, MessageListener messageListener, Supplier queueSupplier, + Function nodeExecutorSupplier, + BiFunction, NodeSink.TimeoutSupplier, Agent> agentSupplier, + Runnable checkFailures, Consumer responseSink, + Supplier randomSupplier, + Supplier timeServiceSupplier, + TopologyFactory topologyFactory, Supplier in, Consumer noMoreWorkSignal, + Consumer> readySignal, BiFunction journalFactory) + { + Topology topology = topologyFactory.toTopology(nodes); + Map nodeMap = new LinkedHashMap<>(); + Map executorMap = new LinkedHashMap<>(); + Map journalMap = new LinkedHashMap<>(); + try + { + RandomSource random = randomSupplier.get(); + Cluster sinks = new Cluster(randomSupplier.get(), messageListener, queueSupplier, checkFailures, nodeMap::get, journalMap::get, () -> topologyFactory.rf, responseSink); + for (Node node : nodeMap.values()) + node.configService().registerListener((ListStore)node.commandStores().dataStore()); + + TopologyUpdates topologyUpdates = new TopologyUpdates(executorMap::get); + TopologyRandomizer.Listener schemaApply = t -> { + for (Node node : nodeMap.values()) + { + ListStore store = (ListStore) node.commandStores().dataStore(); + store.onTopologyUpdate(node, t); + } + messageListener.onTopologyChange(t); + }; + NodeSink.TimeoutSupplier timeouts = new NodeSink.TimeoutSupplier() + { + final RandomSource random = randomSupplier.get(); + // TODO (testing): slow/expires should be broadly in sync with our link latency config + final LongSupplier slowAt, expiresAt, failsAt; + { + int medianSlowAt = random.nextInt(100, 200); + int medianExpiresAt = random.nextInt(1000, 2000); + int medianFailsAt = random.nextInt(1000, 2000); + + int minSlowAt = random.nextInt(0, 100); + int minExpiresAt = random.nextBiasedInt(500, 800, 1000); + int minFailsAt = random.nextBiasedInt(500, 800, 1000); + + int maxSlowAt = random.nextBiasedInt(medianSlowAt + 100, medianSlowAt + 200, 1000); + int maxExpiresAt = random.nextBiasedInt(medianExpiresAt + 500, 3000, 10000); + int maxFailsAt = random.nextBiasedInt(medianFailsAt + 500, 3000, 10000); + + slowAt = random.biasedUniformLongs(minSlowAt, medianSlowAt, maxSlowAt); + expiresAt = random.biasedUniformLongs(minExpiresAt, medianExpiresAt, maxExpiresAt); + failsAt = random.biasedUniformLongs(minFailsAt, medianFailsAt, maxFailsAt); + } + @Override public long slowAt() { return now() + slowAt.getAsLong();} + @Override public long expiresAt() { return now() + expiresAt.getAsLong(); } + @Override public long failsAt() { return now() + failsAt.getAsLong(); } + @Override public long now() { return sinks.pending.nowInMillis(); } + @Override public TimeUnit units() { return MILLISECONDS; } + }; + TopologyRandomizer configRandomizer = new TopologyRandomizer(randomSupplier, prefixes, topology, topologyUpdates, nodeMap::get, schemaApply); + List durabilityService = new ArrayList<>(); + List services = new ArrayList<>(); + for (Id id : nodes) + { + ClusterScheduler scheduler = sinks.new ClusterScheduler(id.id); + MessageSink messageSink = sinks.create(id, timeouts); + TimeService timeService = timeServiceSupplier.get(); + BiConsumer onStale = (sinceAtLeast, ranges) -> configRandomizer.onStale(id, sinceAtLeast, ranges); + AsyncExecutor nodeExecutor = nodeExecutorSupplier.apply(id); + Agent agent = agentSupplier.apply(onStale, timeouts); + executorMap.put(id, nodeExecutor); + Journal journal = journalFactory.apply(id, random); + journalMap.put(id, journal); + BurnTestConfigurationService configService = new BurnTestConfigurationService(id, nodeExecutor, agent, randomSupplier, topology, nodeMap::get, topologyUpdates); + DelayedCommandStores.CacheLoading cacheLoading = new RandomLoader(random).newLoader(journal); + Node node = new Node(id, messageSink, configService, timeService, new AtomicUniqueTimeWithStaleReservation(timeService), + () -> new ListStore(scheduler, random, id), new ShardDistributor.EvenSplit<>(8, ignore -> new PrefixedIntHashKey.Splitter()), + agent, + randomSupplier.get(), scheduler, SizeOfIntersectionSorter.SUPPLIER, DefaultRemoteListeners::new, DefaultTimeouts::new, + DefaultProgressLogs::new, DefaultLocalListeners.Factory::new, DelayedCommandStores.factory(sinks.pending, cacheLoading), new CoordinationAdapter.DefaultFactory(), + journal.durableBeforePersister(), journal); + journal.start(node); + DurabilityService durability = node.durability(); + // TODO (desired): randomise + durability.shards().setShardCycleTime(30, SECONDS); + durability.global().setGlobalCycleTime(180, SECONDS); + durabilityService.add(durability); + nodeMap.put(id, node); + durabilityService.add(new DurabilityService(node)); + } + + Runnable updateDurabilityRate; + { + IntSupplier targetSplits = random.biasedUniformIntsSupplier(1, 16, 2, 4, 4, 16).get(); + IntSupplier shardCycleTimeSeconds = random.biasedUniformIntsSupplier(5, 60, 10, 60, 1, 30).get(); + IntSupplier globalCycleTimeSeconds = random.biasedUniformIntsSupplier(1, 90, 10, 30,10, 60).get(); + updateDurabilityRate = () -> { + int c = targetSplits.getAsInt(); + int s = shardCycleTimeSeconds.getAsInt() * topologyFactory.rf; + int g = globalCycleTimeSeconds.getAsInt(); + durabilityService.forEach(d -> { + d.shards().setTargetShardSplits(c); + d.shards().setShardCycleTime(s, SECONDS); + d.global().setGlobalCycleTime(g, SECONDS); + }); + }; + } + updateDurabilityRate.run(); + schemaApply.onUpdate(topology); + + Pending.Global.setNoActiveOrigin(); + AsyncResult startup = AsyncChains.reduce(nodeMap.values().stream().map(Node::unsafeStart).collect(toList()), (a, b) -> null).beginAsResult(); + Pending.Global.clearActiveOrigin(); + + while (sinks.processPending()); + Invariants.requireArgument(startup.isDone()); + + ClusterScheduler clusterScheduler = sinks.new ClusterScheduler(-1); + List nodesList = new ArrayList<>(Arrays.asList(nodes)); + Scheduled chaos = clusterScheduler.recurring(() -> { + sinks.links = sinks.linkConfig.overrideLinks.apply(nodesList); + if (random.decide(0.1f)) + updateDurabilityRate.run(); + }, 5L, SECONDS); + + Scheduled reconfigure = clusterScheduler.recurring(configRandomizer::maybeUpdateTopology, 1, SECONDS); + + Purge purge = new Purge(clusterScheduler, random, nodesList, nodeMap, journalMap); + + Scheduled bounce = clusterScheduler.recurring(() -> { + Id id = pickNodeNotBootstrapping(random, nodesList, nodeMap); + if (id == null) + return; + + Node node = nodeMap.get(id); + CommandStores stores = node.commandStores(); + while (sinks.drain(getPendingPredicate(id, stores.all()))) ; + + boolean rebootstrap = random.nextBoolean() && !overlapsWithBootstrapping(node, nodeMap); + trace.debug(String.format("Triggering %s for node %s", + rebootstrap ? "rebootstrap" : "bounce and journal replay", + id)); + CommandsForKey.disableLinearizabilityViolationsReporting(); + + // Clean data and restore from snapshot + ListStore listStore = (ListStore) node.commandStores().dataStore(); + NavigableMap> prevData = listStore.copyOfCurrentData(); + listStore.clear(); + listStore.restoreFromSnapshot(); + + // We are simulating node restart, so its remote listeners will also be gone + ((DefaultRemoteListeners) node.remoteListeners()).clear(); + Int2ObjectHashMap> beforeStores = copyCommands(stores.all()); + + Journal journal = journalMap.get(id); + + Journal.TopologyUpdate lastUpdate = null; + { + Iterator iter = journal.replayTopologies(); + while (iter.hasNext()) + { + Journal.TopologyUpdate update = iter.next(); + Invariants.require(lastUpdate == null || update.global.epoch() > lastUpdate.global.epoch()); + lastUpdate = update; + } + + // Reset and restore command store states + for (CommandStore store : stores.all()) + { + DelayedCommandStore store1 = ((DelayedCommandStore) store); + CommandStores.RangesForEpoch beforeRestore = store1.unsafeGetRangesForEpoch(); + store1.unsafeClearForTesting(); + if (lastUpdate != null) + store1.unsafeSetRangesForEpoch(lastUpdate.commandStores.get(store.id())); + CommandStores.RangesForEpoch afterRestore = store1.unsafeGetRangesForEpoch(); + if (!beforeRestore.equals(afterRestore)) + Invariants.require(beforeRestore.equals(afterRestore)); + } + + if (lastUpdate != null) + node.commandStores().resetTopology(lastUpdate); + } + + if (rebootstrap) + { + node.durability().stop(); + ((InMemoryJournal)journal).dropAll(); + + stores.rebootstrap(node).beginAsResult(); + while (sinks.drain(getPendingPredicate(id, stores.all()))) ; + Invariants.require(verifyBootstrapping(node), "Node %s should have been bootstrapping", node); + CommandsForKey.enableLinearizabilityViolationsReporting(); + node.durability().start(); + } + else + { + if (lastUpdate != null) + ((DelayedCommandStores) node.commandStores()).validateShardStateForTesting(lastUpdate); + journal.replay(stores); + while (sinks.drain(getPendingPredicate(id, stores.all()))) ; + CommandsForKey.enableLinearizabilityViolationsReporting(); + + verifyConsistentRestore(beforeStores, stores.all()); + // we can get ahead of prior state by executing further if we skip some earlier phase's dependencies + listStore.checkAtLeast(stores, prevData); + } + trace.debug("Done with replay."); + + }, () -> random.nextInt(10, 30), SECONDS); + + durabilityService.forEach(DurabilityService::start); + services.forEach(Service::start); + + Runnable stop = () -> { + reconfigure.cancel(); + durabilityService.forEach(DurabilityService::stop); + purge.cancel(); + bounce.cancel(); + services.forEach(Service::close); + chaos.cancel(); + sinks.links = sinks.linkConfig.defaultLinks; + }; + noMoreWorkSignal.accept(stop); + readySignal.accept(nodeMap); + + Packet next; + while ((next = in.get()) != null) + sinks.add(next, 0, TimeUnit.NANOSECONDS); + + while (sinks.processPending()); + + stop.run(); + + // give progress log et al a chance to finish + // TODO (desired, testing): would be nice to make this more certain than an arbitrary number of additional rounds + for (int i = 0 ; i < 10 ; ++i) + { + sinks.processAll(); + while (sinks.processPending()); + } + + while (!sinks.onDone.isEmpty()) + { + List onDone = new ArrayList<>(sinks.onDone); + sinks.onDone.clear(); + onDone.forEach(Runnable::run); + while (sinks.processPending()); + } + + return sinks.statsMap; + } + finally + { + nodeMap.values().forEach(Node::shutdown); + } + } + + private static class Purge + { + Scheduled scheduled; + + Purge(Scheduler clusterScheduler, RandomSource rs, List nodes, Map nodeMap, Map journalMap) + { + schedule(clusterScheduler, rs, nodes, nodeMap, journalMap); + } + + void cancel() + { + scheduled.cancel(); + } + + private void schedule(Scheduler clusterScheduler, RandomSource rs, List nodes, Map nodeMap, Map journalMap) + { + scheduled = clusterScheduler.selfRecurring(() -> run(clusterScheduler, rs, nodes, nodeMap, journalMap), rs.nextInt(1, 2), SECONDS); + } + + private void run(Scheduler clusterScheduler, RandomSource rs, List nodes, Map nodeMap, Map journalMap) + { + Id id = rs.pick(nodes); + Node node = nodeMap.get(id); + + Journal journal = journalMap.get(node.id()); + CommandStores stores = nodeMap.get(node.id()).commandStores(); + // run on node scheduler so doesn't run during replay + scheduled = node.scheduler().selfRecurring(() -> { + journal.purge(stores, node.topology()::minEpoch); + schedule(clusterScheduler, rs, nodes, nodeMap, journalMap); + }, 0, SECONDS); + } + + } + + private static Int2ObjectHashMap> copyCommands(CommandStore[] stores) + { + Int2ObjectHashMap> result = new Int2ObjectHashMap<>(); + for (CommandStore s : stores) + { + DelayedCommandStores.DelayedCommandStore store = (DelayedCommandStores.DelayedCommandStore) s; + NavigableMap commands = new TreeMap<>(); + result.put(store.id(), commands); + for (Map.Entry e : store.unsafeCommands().entrySet()) + { + Command command = e.getValue().value(); + Invariants.require(command.saveStatus() != SaveStatus.Uninitialised, + "Found uninitialized command in the log: %s", command); + commands.put(e.getKey(), command); + } + + } + return result; + } + + private static boolean verifyBootstrapping(Node node) + { + CommandStore[] stores = node.commandStores().all(); + return Stream.of(stores).allMatch(CommandStore::isBootstrapping); + } + + private static boolean overlapsWithBootstrapping(Node pick, Map nodeMap) + { + Ranges localRanges = pick.commandStores().local().ranges(); + for (Map.Entry e : nodeMap.entrySet()) + { + for (CommandStore commandStore : e.getValue().commandStores().all()) + { + if (commandStore.isBootstrapping() && + (e.getKey().equals(pick.id()) || commandStore.unsafeGetRangesForEpoch().all().intersects(localRanges))) + return true; + } + } + return false; + } + + private static Id pickNodeNotBootstrapping(RandomSource random, List ids, Map nodeMap) + { + List remaining = new ArrayList<>(ids); + while (!remaining.isEmpty()) + { + int i = random.nextInt(remaining.size()); + Id id = remaining.get(i); + CommandStore[] stores = nodeMap.get(id).commandStores().all(); + if (!Stream.of(stores).anyMatch(cs -> cs.isBootstrapping() || cs.isRebootstrapping())) + return id; + + remaining.set(i, remaining.get(remaining.size() - 1)); + remaining.remove(remaining.size() - 1); + } + return null; + } + + private static void verifyConsistentRestore(Int2ObjectHashMap> beforeStores, CommandStore[] stores) + { + for (CommandStore s : stores) + { + DelayedCommandStores.DelayedCommandStore store = (DelayedCommandStores.DelayedCommandStore) s; + NavigableMap before = beforeStores.get(store.id()); + for (Map.Entry e : store.unsafeCommands().entrySet()) + { + Command beforeCommand = before.get(e.getKey()); + Command afterCommand = e.getValue().value(); + if (beforeCommand == null) + { + Invariants.require(afterCommand.is(Status.NotDefined) || afterCommand.saveStatus().compareTo(SaveStatus.Vestigial) >= 0); + continue; + } + if (afterCommand.hasBeen(Status.Truncated)) + { + if (afterCommand.is(Status.Invalidated)) + Invariants.require(beforeCommand.hasBeen(Status.Truncated) || (!beforeCommand.hasBeen(Status.PreCommitted) + && Cleanup.shouldCleanup(FULL, e.getKey(), beforeCommand.executeAtIfKnown(), beforeCommand.saveStatus(), beforeCommand.durability(), afterCommand.participants(), store.unsafeGetRedundantBefore(), store.durableBefore()).compareTo(INVALIDATE) >= 0)); + continue; + } + if (beforeCommand.hasBeen(Status.Truncated)) + { + Invariants.require(!beforeCommand.is(Status.Invalidated) || afterCommand.is(Status.Invalidated)); + Invariants.require(beforeCommand.is(Status.Invalidated) || afterCommand.is(Status.Truncated) || afterCommand.is(Status.Applied)); + continue; + } + Invariants.require(isConsistent(beforeCommand.saveStatus(), afterCommand.saveStatus()), + "%s != %s", beforeCommand.saveStatus(), afterCommand.saveStatus()); + Invariants.require(beforeCommand.executeAtOrTxnId().equals(afterCommand.executeAtOrTxnId()), + "%s != %s", beforeCommand.executeAtOrTxnId(), afterCommand.executeAtOrTxnId()); + Invariants.require(beforeCommand.acceptedOrCommitted().equals(afterCommand.acceptedOrCommitted()), + "%s != %s", beforeCommand.acceptedOrCommitted(), afterCommand.acceptedOrCommitted()); + Invariants.require(beforeCommand.promised().equals(afterCommand.promised()), + "%s != %s", beforeCommand.promised(), afterCommand.promised()); + Invariants.require(beforeCommand.durability().equals(afterCommand.durability()), + "%s != %s", beforeCommand.durability(), afterCommand.durability()); + } + + if (before.size() > store.unsafeCommands().size()) + { + for (Map.Entry entry : before.entrySet()) + { + TxnId txnId = entry.getKey(); + if (!store.unsafeCommands().containsKey(txnId)) + { + Command beforeCommand = entry.getValue(); + if (beforeCommand.saveStatus() == SaveStatus.Erased) + continue; + + if (Cleanup.shouldCleanup(FULL, beforeCommand, store.unsafeGetRedundantBefore(), store.durableBefore()) == EXPUNGE) + continue; + + if (store.unsafeGetRedundantBefore().min(beforeCommand.participants().owns(), RedundantBefore.Bounds::shardAndLocallyRedundantBefore).compareTo(txnId) > 0) + continue; + + if (beforeCommand.participants().owns().isEmpty() && store.durableBefore().min(txnId).compareTo(Status.Durability.MajorityOrInvalidated) >= 0) + continue; + + if (!beforeCommand.saveStatus().hasBeen(Status.PreCommitted) && store.unsafeGetRedundantBefore().min(beforeCommand.participants().owns(), RedundantBefore.Bounds::locallyRedundantBefore).compareTo(txnId) > 0) + continue; + + Invariants.require(false, "Found a command in an unexpected state: %s", beforeCommand); + } + } + } + } + } + + private static boolean isConsistent(SaveStatus before, SaveStatus after) + { + if (before == after) + return true; + + if (before == SaveStatus.Uninitialised || before == SaveStatus.NotDefined) + return after == SaveStatus.Uninitialised || after == SaveStatus.NotDefined; + + // depending on arrival order, an unmanaged txn may be ready to execute immediately or have to wait for another transaction to commit + if (before == SaveStatus.Stable || before == SaveStatus.ReadyToExecute) + return after == SaveStatus.Stable || after == SaveStatus.ReadyToExecute; + + if (before == SaveStatus.PreApplied || before == SaveStatus.Applying || before == SaveStatus.Applied) + return after == SaveStatus.PreApplied || after == SaveStatus.Applying || after == SaveStatus.Applied; + + return false; + } + + private static Predicate getPendingPredicate(Id nodeId, CommandStore[] stores) + { + Set nodeStores = new HashSet<>(Arrays.asList(stores)); + return item -> { + if (item instanceof DelayedCommandStore.DelayedTask) + { + DelayedCommandStore.DelayedTask task = (DelayedCommandStore.DelayedTask) item; + if (nodeStores.contains(task.owner())) + return true; + item = item.origin(); + } + if (item instanceof SimulatedDelayedExecutorService.RegularTask) + { + SimulatedDelayedExecutorService.RegularTask task = (SimulatedDelayedExecutorService.RegularTask) item; + Object owner = task.owner(); + if (owner != null && owner.equals(nodeId)) + return true; + item = item.origin(); + } + if (item instanceof RecurringPendingRunnable) + { + RecurringPendingRunnable recurring = (RecurringPendingRunnable) item; + return recurring.source == nodeId.id && (!recurring.isRecurring || recurring.origin() != recurring); + } + return false; + }; + } + + private interface Service extends AutoCloseable + { + void start(); + @Override + void close(); + } + + private static BiFunction partition(List nodes, RandomSource random, int rf, BiFunction up) + { + Collections.shuffle(nodes, random.asJdkRandom()); + int partitionSize = random.nextInt((rf+1)/2); + Set partition = new LinkedHashSet<>(nodes.subList(0, partitionSize)); + BiFunction down = (from, to) -> new Link(() -> DROP, up.apply(from, to).latencyMicros); + return (from, to) -> (partition.contains(from) == partition.contains(to) ? up : down).apply(from, to); + } + + /** + * pair every node with one other node in one direction with a network behaviour override + */ + private static BiFunction pairedUnidirectionalOverrides(Function linkOverride, List nodes, RandomSource random, BiFunction fallback) + { + Map> map = new HashMap<>(); + Collections.shuffle(nodes, random.asJdkRandom()); + for (int i = 0 ; i + 1 < nodes.size() ; i += 2) + { + Id from = nodes.get(i); + Id to = nodes.get(i + 1); + Link link = linkOverride.apply(fallback.apply(from, to)); + map.put(from, singletonMap(to, link)); + } + return (from, to) -> nonNullOrGet(map.getOrDefault(from, emptyMap()).get(to), from, to, fallback); + } + + private static BiFunction randomOverrides(boolean bidirectional, Function linkOverride, int count, List nodes, RandomSource random, BiFunction fallback) + { + Map> map = new HashMap<>(); + while (count > 0) + { + Id from = nodes.get(random.nextInt(nodes.size())); + Id to = nodes.get(random.nextInt(nodes.size())); + Link fwd = linkOverride.apply(fallback.apply(from, to)); + if (null == map.computeIfAbsent(from, ignore -> new HashMap<>()).putIfAbsent(to, fwd)) + { + if (bidirectional) + { + Link rev = linkOverride.apply(fallback.apply(to, from)); + map.computeIfAbsent(to, ignore -> new HashMap<>()).put(from, rev); + } + --count; + } + } + return (from, to) -> nonNullOrGet(map.getOrDefault(from, emptyMap()).get(to), from, to, fallback); + } + + private static Link nonNullOrGet(Link ifNotNull, Id from, Id to, BiFunction function) + { + if (ifNotNull != null) + return ifNotNull; + return function.apply(from, to); + } + + private static Link healthy(LongSupplier latency) + { + return new Link(() -> DELIVER, latency); + } + + private static Link down(LongSupplier latency) + { + return new Link(() -> DROP, latency); + } + + private LongSupplier defaultRandomWalkLatencyMicros(RandomSource random) + { + LongSupplier range = FrequentLargeRange.builder(random) + .ratio(1, 5) + .small(500, TimeUnit.MICROSECONDS, 5, MILLISECONDS) + .large(50, MILLISECONDS, 5, SECONDS) + .build().asLongSupplier(random); + + return () -> NANOSECONDS.toMicros(range.getAsLong()); + } + + enum OverrideLinkKind { LATENCY, ACTION, BOTH } + + private Supplier> linkOverrideSupplier(RandomSource random) + { + Supplier nextKind = random.randomWeightedPicker(OverrideLinkKind.values()); + Supplier latencySupplier = random.biasedUniformLongsSupplier( + MILLISECONDS.toMicros(1L), SECONDS.toMicros(2L), + MILLISECONDS.toMicros(1L), MILLISECONDS.toMicros(300L), SECONDS.toMicros(1L), + MILLISECONDS.toMicros(1L), MILLISECONDS.toMicros(300L), SECONDS.toMicros(1L) + ); + NodeSink.Action[] actions = NodeSink.Action.values(); + Supplier> actionSupplier = () -> random.randomWeightedPicker(actions); + return () -> { + OverrideLinkKind kind = nextKind.get(); + switch (kind) + { + default: throw new UnhandledEnum(kind); + case BOTH: return ignore -> new Link(actionSupplier.get(), latencySupplier.get()); + case ACTION: return override -> new Link(actionSupplier.get(), override.latencyMicros); + case LATENCY: return override -> new Link(override.action, latencySupplier.get()); + } + }; + } + + enum OverrideLinksKind { NONE, PAIRED_UNIDIRECTIONAL, RANDOM_UNIDIRECTIONAL, RANDOM_BIDIRECTIONAL } + + private Function, BiFunction> overrideLinks(RandomSource random, IntSupplier rf, BiFunction defaultLinks) + { + Supplier> linkOverrideSupplier = linkOverrideSupplier(random); + BooleanSupplier partitionChance = random.biasedUniformBools(random.nextFloat()); + Supplier nextKind = random.randomWeightedPicker(OverrideLinksKind.values()); + return nodesList -> { + BiFunction links = defaultLinks; + if (partitionChance.getAsBoolean()) // 50% chance of a whole network partition + links = partition(nodesList, random, rf.getAsInt(), links); + + OverrideLinksKind kind = nextKind.get(); + if (kind == NONE) + return links; + + Function linkOverride = linkOverrideSupplier.get(); + switch (kind) + { + default: throw new UnhandledEnum(kind); + case PAIRED_UNIDIRECTIONAL: + return pairedUnidirectionalOverrides(linkOverride, nodesList, random, defaultLinks); + case RANDOM_BIDIRECTIONAL: + case RANDOM_UNIDIRECTIONAL: + boolean bidirectional = kind == RANDOM_BIDIRECTIONAL; + int count = random.nextInt(bidirectional || random.nextBoolean() ? nodesList.size() : Math.max(1, (nodesList.size() * nodesList.size())/2)); + return randomOverrides(bidirectional, linkOverride, count, nodesList, random, defaultLinks); + } + }; + } + + private BiFunction defaultLinks(RandomSource random) + { + return caching((from, to) -> healthy(defaultRandomWalkLatencyMicros(random))); + } + + private BiFunction caching(BiFunction uncached) + { + Map> stash = new HashMap<>(); + return (from, to) -> stash.computeIfAbsent(from, ignore -> new HashMap<>()) + .computeIfAbsent(to, ignore -> uncached.apply(from, to)); + } + + private LinkConfig defaultLinkConfig(RandomSource random, IntSupplier rf) + { + BiFunction defaultLinks = defaultLinks(random); + Function, BiFunction> overrideLinks = overrideLinks(random, rf, defaultLinks); + return new LinkConfig(overrideLinks, defaultLinks); + } + + public static class BlockingTransaction + { + final TxnId txnId; + final Command command; + final DelayedCommandStore commandStore; + final Command blockedOn; + final Object blockedVia; + + public BlockingTransaction(TxnId txnId, Command command, DelayedCommandStore commandStore, @Nullable Command blockedOn, @Nullable Object blockedVia) + { + this.txnId = txnId; + this.command = command; + this.commandStore = commandStore; + this.blockedOn = blockedOn; + this.blockedVia = blockedVia; + Invariants.requireArgument(blockedOn == null || !txnId.equals(blockedOn.txnId())); + } + + @Override + public String toString() + { + return txnId + ":" + command.saveStatus() + "@" + + commandStore.toString().replaceAll("DelayedCommandStore", "") + + (command.homeKey() != null && commandStore.unsafeGetRangesForEpoch().allAt(txnId.epoch()).contains(command.homeKey()) ? "(Home)" : ""); + } + } + + public List findBlockedCommitted(@Nullable Txn.Kind first, Txn.Kind ... rest) + { + return findBlocked(SaveStatus.Committed, first, rest); + } + + public List findBlocked(@Nullable SaveStatus minSaveStatus, @Nullable Txn.Kind first, Txn.Kind ... rest) + { + List result = new ArrayList<>(); + BlockingTransaction cur = findMin(SaveStatus.Committed, SaveStatus.ReadyToExecute, first, rest); + while (cur != null) + { + result.add(cur); + Command command = cur.commandStore.unsafeCommands().get(cur.txnId).value(); + if (!command.hasBeen(Status.Stable) || cur.blockedOn == null) + break; + + cur = find(cur.blockedOn.txnId(), null, SaveStatus.Stable); + } + return result; + } + + public BlockingTransaction findMinUnstable() + { + return findMin(true, null, SaveStatus.Committed, null); + } + + public BlockingTransaction findMinUnstable(@Nullable Txn.Kind first, Txn.Kind ... rest) + { + return findMin(null, SaveStatus.Committed, first, rest); + } + + public BlockingTransaction findMin(@Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus, @Nullable Txn.Kind first, Txn.Kind ... rest) + { + Predicate testKind = first == null ? ignore -> true : EnumSet.of(first, rest)::contains; + return findMin(minSaveStatus, maxSaveStatus, id -> testKind.test(id.kind())); + } + + public BlockingTransaction find(TxnId txnId, @Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus) + { + return findMin(minSaveStatus, maxSaveStatus, txnId::equals); + } + + public BlockingTransaction find(boolean onlyIfOwned, TxnId txnId, @Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus) + { + return findMin(onlyIfOwned, minSaveStatus, maxSaveStatus, txnId::equals); + } + + public List findTransitivelyBlocking(TxnId txnId) + { + return findTransitivelyBlocking(true, txnId); + } + + public List findTransitivelyBlocking(boolean onlyIfOwned, TxnId txnId) + { + BlockingTransaction txn = find(onlyIfOwned, txnId, null, null); + if (txn == null) + return null; + + List result = new ArrayList<>(); + while (true) + { + result.add(txn); + if (txn.command.saveStatus().compareTo(SaveStatus.Stable) < 0) + return result; + + if (txn.blockedOn == null) + { + // look for another copy that is still blocked, and continue from there + txn = find(txn.txnId, null, null); + if (txn.blockedOn == null) + return result; + } + + Command blockedOn = txn.blockedOn; + GlobalCommand command = txn.commandStore.unsafeCommands().get(blockedOn.txnId()); + if (command == null) + return result; + else if (command.value().saveStatus().compareTo(SaveStatus.Applied) < 0) + txn = toBlocking(command.value(), txn.commandStore); + else + txn = find(txn.blockedOn.txnId(), null, null); + } + } + + public BlockingTransaction findMin(@Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus, Predicate testTxnId) + { + return findMin(false, minSaveStatus, maxSaveStatus, testTxnId); + } + + public BlockingTransaction findMin(boolean onlyIfOwned, @Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus, Predicate testTxnId) + { + return find(onlyIfOwned, minSaveStatus, maxSaveStatus, testTxnId, (min, test) -> { + int c = -1; + if (min == null || (c = test.txnId.compareTo(min.txnId)) <= 0 && (c < 0 || test.command.saveStatus().compareTo(min.command.saveStatus()) < 0)) + min = test; + return min; + }, null); + } + + public List findAll(TxnId txnId) + { + return findAll(null, null, txnId::equals); + } + + public List findAll(@Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus, Predicate testTxnId) + { + List result = new ArrayList<>(); + find(false, minSaveStatus, maxSaveStatus, testTxnId, (r, c) -> { r.add(c); return r; }, result); + return result; + } + + public T find(boolean onlyIfOwned, @Nullable SaveStatus minSaveStatus, @Nullable SaveStatus maxSaveStatus, Predicate testTxnId, BiFunction fold, T accumulate) + { + for (Node.Id id : sinks.keySet()) + { + Node node = lookup.apply(id); + + DelayedCommandStores stores = (DelayedCommandStores) node.commandStores(); + for (DelayedCommandStore store : stores.unsafeStores()) + { + for (Map.Entry e : store.unsafeCommands().entrySet()) + { + Command command = e.getValue().value(); + if ((!onlyIfOwned || owns(command, store)) && + (minSaveStatus == null || command.saveStatus().compareTo(minSaveStatus) >= 0) && + (maxSaveStatus == null || command.saveStatus().compareTo(maxSaveStatus) <= 0) && + (testTxnId == null || testTxnId.test(command.txnId()))) + { + accumulate = fold.apply(accumulate, toBlocking(command, store)); + break; + } + } + } + } + return accumulate; + } + + private boolean owns(Command command, CommandStore commandStore) + { + StoreParticipants participants = command.participants(); + if (participants == null) + return false; + + return participants.owns().intersects(commandStore.unsafeGetRangesForEpoch().allBetween(command.txnId().epoch(), command.executeAtIfKnownElseTxnId())); + } + + private BlockingTransaction toBlocking(Command command, DelayedCommandStore store) + { + Object blockedVia = null; + TxnId blockedOnId = null; + if (command.hasBeen(Status.Stable) && !command.hasBeen(Status.Truncated)) + { + Command.WaitingOn waitingOn = command.asCommitted().waitingOn(); + RoutingKey blockedOnKey = waitingOn.lastWaitingOnKey(); + if (blockedOnKey == null) + { + blockedOnId = waitingOn.nextWaitingOn(); + Invariants.require(!command.txnId().equals(blockedOnId)); + if (blockedOnId != null) + blockedVia = command.partialDeps().participants(blockedOnId); + } + else + { + CommandsForKey cfk = store.unsafeCommandsForKey().get(blockedOnKey).value(); + blockedOnId = cfk.blockedOnTxnId(command.txnId(), command.executeAt()); + if (blockedOnId != null) + blockedVia = cfk; + Invariants.require(!command.txnId().equals(blockedOnId)); + } + } + Command blockedOn = null; + if (blockedOnId != null) + { + GlobalCommand cmd = store.unsafeCommands().get(blockedOnId); + if (cmd == null) blockedOn = uninitialised(blockedOnId); + else blockedOn = cmd.value(); + } + return new BlockingTransaction(command.txnId(), command, store, blockedOn, blockedVia); + } + +} diff --git a/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java b/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java index e17b8e5bd4..4779488b7f 100644 --- a/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java +++ b/accord-core/src/test/java/accord/impl/basic/DelayedCommandStores.java @@ -303,6 +303,12 @@ protected boolean canExposeUnloaded() return !cacheLoading.cacheEmpty(); } + @Override + public GlobalCommandsForKey commandsForKey(String key) + { + return commandsForKey(PrefixedIntHashKey.Hash.fromString(key)); + } + private static CommandStore.Factory factory(SimulatedDelayedExecutorService executor, CacheLoading isLoadedCheck) { return (id, node, agent, store, progressLogFactory, listenersFactory, rangesForEpoch, journal) -> new DelayedCommandStore(id, node, agent, store, progressLogFactory, listenersFactory, rangesForEpoch, executor, isLoadedCheck, journal); diff --git a/accord-core/src/test/java/accord/local/RedundantBeforeTest.java b/accord-core/src/test/java/accord/local/RedundantBeforeInfoTest.java similarity index 99% rename from accord-core/src/test/java/accord/local/RedundantBeforeTest.java rename to accord-core/src/test/java/accord/local/RedundantBeforeInfoTest.java index c0b0fec478..bc8c76009b 100644 --- a/accord-core/src/test/java/accord/local/RedundantBeforeTest.java +++ b/accord-core/src/test/java/accord/local/RedundantBeforeInfoTest.java @@ -49,7 +49,7 @@ import static accord.local.RedundantStatus.selectOrCreate; import static accord.local.RedundantStatus.toAll; -public class RedundantBeforeTest +public class RedundantBeforeInfoTest { @Test public void test() diff --git a/accord-debug/build.gradle b/accord-debug/build.gradle new file mode 100644 index 0000000000..bcf78f57c5 --- /dev/null +++ b/accord-debug/build.gradle @@ -0,0 +1,42 @@ +/* + * 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. + */ + +plugins { + id 'accord.java-conventions' + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':accord-core') + implementation 'io.javalin:javalin:5.6.2' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.slf4j:slf4j-api:1.7.36' + + runtimeOnly 'ch.qos.logback:logback-classic:1.2.12' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation project(':accord-core').sourceSets.test.output + +} + +application { + mainClass = 'accord.debug.Server' +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/AbstractServer.java b/accord-debug/src/main/java/accord/debug/AbstractServer.java new file mode 100644 index 0000000000..3b84ba9a9c --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/AbstractServer.java @@ -0,0 +1,214 @@ +package accord.debug; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import accord.debug.controller.Controller; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.javalin.Javalin; +import io.javalin.http.Context; +import io.javalin.http.staticfiles.Location; +import io.javalin.json.JavalinGson; + +public abstract class AbstractServer +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractServer.class); + + private final AtomicReference> debugFuture = new AtomicReference<>(); + + private final Javalin app; + private final int port; + private final Controller controller; + + public AbstractServer(int port, Controller controller) + { + this.controller = controller; + this.port = port; + this.app = Javalin.create(config -> { +// appConfig.staticFiles.add("/web", Location.CLASSPATH); + config.staticFiles.add("/Users/ifesdjeen/p/java/cassandra-accord-protocol/accord-debug/src/main/resources/web", Location.EXTERNAL); + config.jsonMapper(createCustomJsonMapper()); + }); + + setupRoutes(); + } + + private static JavalinGson createCustomJsonMapper() + { + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + return new JavalinGson(gson); + } + + private void setupRoutes() + { + app.get("/hosts/{hostname}/redundant_before", this::handleRedundantBefore); + app.get("/hosts/{hostname}/commands_for_keys/{key}", this::handleCommandsForKeys); + app.get("/hosts/{hostname}/transactions/{txnId}", this::handleGetTxn); + app.get("/hosts/{hostname}/stores/{storeId}/transactions", this::handleTransactions); + app.get("/hosts/{hostname}/stores/{storeId}/transactions/{txnId}", this::handleTransaction); + app.get("/hosts/{hostname}/coordinations", this::handleCoordinations); + app.get("/hosts/{hostname}/blocked_by/{txnId}", this::handleTxnBlockedBy); + app.get("/hosts/{hostname}/progress_log", this::handleProgressLog); + app.get("/hosts/{hostname}/durability_service", this::handleDurabilityService); + app.get("/hosts/{hostname}/command_store", this::handleCommandStores); + app.get("/hosts/{hostname}/durable_before", this::handleDurableBefore); + app.get("/hosts/{hostname}/topologies", this::handleTopologies); + app.get("/hosts", this::handleGetHosts); + app.get("/unpause", this::handleUnpause); + } + + public void start() + { + app.start(port); + logger.info("Cluster debug server started on port {}", app.port()); + } + + public void stop() + { + app.stop(); + logger.info("Cluster debug server stopped"); + } + + + private void handleRedundantBefore(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getRedundantBefore(hostname))); + } + + private void handleCommandsForKeys(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + String key = ctx.pathParam("key"); + Response.sendResponse(ctx, Response.compute(() -> controller.getCommandsForKey(hostname, key))); + } + + private void handleGetTxn(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + String txnId = ctx.pathParam("txnId"); + + Response.sendResponse(ctx, Response.compute(() -> controller.getTxn(hostname, txnId))); + } + + private void handleTransactions(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + String property = ctx.queryParam("property"); + Response.sendResponse(ctx, Response.compute(() -> controller.getTransactions(hostname, storeId, property))); + } + + private void handleTransaction(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + String txnId = ctx.pathParam("txnId"); + Response.sendResponse(ctx, Response.compute(() -> controller.getTransaction(hostname, storeId, txnId))); + } + + private void handleCoordinations(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getCoordinations(hostname))); + } + + private void handleTxnBlockedBy(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + String txnId = ctx.pathParam("txnId"); + Response.sendResponse(ctx, Response.compute(() -> controller.getTxnBlockedBy(hostname, txnId))); + } + + private void handleProgressLog(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getProgressLog(hostname))); + } + + private void handleDurabilityService(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getDurabilityService(hostname))); + + } + + private void handleCommandStores(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getCommandStores(hostname))); + } + + private void handleDurableBefore(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getDurableBefore(hostname))); + } + + private void handleTopologies(Context ctx) + { + String hostname = ctx.pathParam("hostname"); + Response.sendResponse(ctx, Response.compute(() -> controller.getTopologies(hostname))); + } + + private void handleGetHosts(Context ctx) + { + Response.sendResponse(ctx, Response.compute(controller::getNodes)); + } + + /** + * Future Management + */ + private void handleUnpause(Context ctx) + { + try + { + CompletableFuture future = debugFuture.getAndSet(null); + if (future != null) future.complete(null); + Response.sendResponse(ctx, Response.success("Debug session ended")); + logger.info("Debug session ended via /unpause endpoint"); + } + catch (Exception e) + { + logger.error("Error ending debug session", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + public void pause() + { + try + { + pauseInternal().get(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + private CompletableFuture pauseInternal() + { + while (true) + { + CompletableFuture future = debugFuture.get(); + if (future == null) + { + future = new CompletableFuture<>(); + if (debugFuture.compareAndSet(null, future)) + return future; + + future.cancel(true); + } + else + return future; + } + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/DeprecatedController.java b/accord-debug/src/main/java/accord/debug/DeprecatedController.java new file mode 100644 index 0000000000..8dbc8f04cf --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/DeprecatedController.java @@ -0,0 +1,495 @@ +/* + * 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 accord.debug; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; + +import accord.impl.InMemoryCommandStore; +import accord.impl.InMemoryCommandStore.GlobalCommand; +import accord.impl.InMemoryCommandStore.GlobalCommandsForKey; +import accord.local.Command; +import accord.local.cfk.CommandsForKey; +import accord.local.CommandStore; +import accord.local.CommandStores; +import accord.local.Node; +import accord.local.RedundantBefore; +import accord.local.RedundantStatus; +import accord.api.RoutingKey; +import accord.primitives.Range; +import accord.primitives.TxnId; + +public class DeprecatedController +{ + public static Response> getNodesWithStores(Map nodes) + { + try + { + List nodesWithStores = new ArrayList<>(); + + for (Map.Entry entry : nodes.entrySet()) + { + int nodeId = entry.getKey(); + Node node = entry.getValue(); + + List stores = new ArrayList<>(); + + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + for (int i = 0; i < allStores.length; i++) + { + CommandStore store = allStores[i]; + if (store != null) + { + List ranges = new ArrayList<>(); + for (Range range : store.unsafeGetRangesForEpoch().all()) + ranges.add(range); + stores.add(new Model.StoreInfo(store.id(), ranges)); + } + } + } + catch (Exception e) + { + // Log warning but continue with empty store list for this node + } + + nodesWithStores.add(new Model.NodeInfo(nodeId, stores)); + } + + nodesWithStores.sort((a, b) -> Integer.compare(a.id, b.id)); + return Response.success(nodesWithStores); + } + catch (Exception e) + { + return Response.failure("Error getting nodes with stores: " + e.getMessage()); + } + } + + public static Response> getNodesByRange(Map nodes, String rangeString) + { + try + { + Range range = parseRange(rangeString); + List nodesWithStores = new ArrayList<>(); + + for (Map.Entry entry : nodes.entrySet()) + { + int nodeId = entry.getKey(); + Node node = entry.getValue(); + + List filteredStores = new ArrayList<>(); + + for (CommandStore store : node.commandStores().all()) + { + if (store != null) + { + List matchingRanges = new ArrayList<>(); + + // Find ranges that intersect with the parsed range + for (Range storeRange : store.unsafeGetRangesForEpoch().all()) + { + if (range.compareIntersecting(storeRange) == 0) + matchingRanges.add(storeRange); + } + + // Only include store if it has matching ranges + if (!matchingRanges.isEmpty()) + { + filteredStores.add(new Model.StoreInfo(store.id(), matchingRanges)); + } + } + } + + // Only include node if it has stores with matching ranges + if (!filteredStores.isEmpty()) + { + nodesWithStores.add(new Model.NodeInfo(nodeId, filteredStores)); + } + } + + nodesWithStores.sort((a, b) -> Integer.compare(a.id, b.id)); + return Response.success(nodesWithStores); + } + catch (Exception e) + { + return Response.failure("Error filtering by range string '" + rangeString + "': " + e.getMessage()); + } + } + + public static Range parseRange(String fromString) + { + try + { + // Parse range format: "prefix:(start,end]" or "prefix:[start,end)" + int colonIndex = fromString.indexOf(':'); + if (colonIndex < 0) + { + throw new IllegalArgumentException("Invalid range format, expected 'prefix:(start,end]': " + fromString); + } + + String prefixStr = fromString.substring(0, colonIndex); + String rangeStr = fromString.substring(colonIndex + 1); + + // Parse the bracket notation + boolean startInclusive = rangeStr.startsWith("["); + boolean endInclusive = rangeStr.endsWith("]"); + + if (!startInclusive && !rangeStr.startsWith("(")) + throw new IllegalArgumentException("Range must start with [ or ("); + if (!endInclusive && !rangeStr.endsWith(")")) + throw new IllegalArgumentException("Range must end with ] or )"); + + // Extract the content between brackets + String content = rangeStr.substring(1, rangeStr.length() - 1); + String[] parts = content.split(",", 2); + if (parts.length != 2) + throw new IllegalArgumentException("Range must contain exactly one comma"); + + int prefix = Integer.parseInt(prefixStr); + int start = Integer.parseInt(parts[0].trim()); + int end = Integer.parseInt(parts[1].trim()); + + // Use reflection to call the PrefixedIntHashKey.range(prefix, start, end) method + try + { + Class prefixedIntHashKeyClass = Class.forName("accord.impl.PrefixedIntHashKey"); + java.lang.reflect.Method rangeMethod = prefixedIntHashKeyClass.getMethod("range", int.class, int.class, int.class); + return (Range) rangeMethod.invoke(null, prefix, start, end); + } + catch (Exception e) + { + throw new IllegalArgumentException("Failed to create PrefixedIntHashKey range", e); + } + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("Invalid number format in range: " + fromString, e); + } + } + + public static Response> getStoresList(Node node) + { + List stores = new ArrayList<>(); + + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + for (int i = 0; i < allStores.length; i++) + { + CommandStore store = allStores[i]; + if (store != null) + { + List ranges = new ArrayList<>(); + for (Range range : store.unsafeGetRangesForEpoch().all()) + ranges.add(range); + stores.add(new Model.StoreInfo(store.id(), ranges)); + } + } + } + catch (Exception e) + { + return Response.failure("Error accessing stores: " + e.getMessage()); + } + + return Response.success(stores); + } + + public static Response> getTransactionsList(Node node, int storeId, String propertyFilter) + { + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + if (storeId < 0 || storeId >= allStores.length || allStores[storeId] == null) + { + return Response.failure("Store " + storeId + " not found"); + } + + // For now only for BurnTest + InMemoryCommandStore store = (InMemoryCommandStore) allStores[storeId]; + CommandStore commandStore = allStores[storeId]; + + // Get redundant before data for property filtering + RedundantBefore redundantBefore = null; + RedundantStatus.Property property = null; + if (propertyFilter != null && !propertyFilter.trim().isEmpty()) + { + try + { + property = RedundantStatus.Property.valueOf(propertyFilter.trim()); + redundantBefore = commandStore.unsafeGetRedundantBefore(); + } + catch (IllegalArgumentException e) + { + return Response.failure("Invalid property: " + propertyFilter + ". Valid properties: " + + java.util.Arrays.toString(RedundantStatus.Property.values())); + } + } + + List transactions = new ArrayList<>(); + NavigableMap commands = store.unsafeCommands(); + + try + { + for (GlobalCommand globalCommand : commands.values()) + { + Command command = globalCommand.value(); + if (command == null) + continue; + + // Apply property filter if specified + boolean satisfiesProperty = false; + if (property != null && redundantBefore != null) + satisfiesProperty = RedundantBefore.satisfies(redundantBefore, command.txnId(), command.route().homeKey(), property); + + Model.TxnInfo txnInfo = new Model.TxnInfo(command.txnId(), + command.route() == null ? null : command.route().homeKey(), + safeToString(command.participants()), + command.saveStatus(), + command.durability(), + command.executeAt(), + command.promised(), + command.acceptedOrCommitted(), + command.partialDeps() == null ? null : command.partialDeps().asListUnsafe(), + command.waitingOn() == null? null : command.waitingOn().asListUnsafe(), + satisfiesProperty); + transactions.add(txnInfo); + } + } + catch (Exception e) + { + return Response.failure(e.getMessage()); + } + return Response.success(transactions); + } + catch (Exception e) + { + return Response.failure("Failed to access command store: " + e.getMessage()); + } + } + + public static Response getSingleTransaction(Node node, int storeId, String txnIdStr) + { + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + if (storeId < 0 || storeId >= allStores.length || allStores[storeId] == null) + { + return Response.failure("Store " + storeId + " not found"); + } + + // For now only for BurnTest + InMemoryCommandStore store = (InMemoryCommandStore) allStores[storeId]; + + NavigableMap commands = store.unsafeCommands(); + + // TODO: use journal instead! + + // Find the specific transaction + for (GlobalCommand globalCommand : commands.values()) + { + try + { + Command command = globalCommand.value(); + if (command == null) continue; + + // Check if this is the transaction we're looking for + if (txnIdStr.equals(command.txnId().toString())) + { + Model.TxnInfo txnInfo = new Model.TxnInfo(command.txnId(), + command.route().homeKey(), + // TODO: turn into something more digestable + safeToString(command.participants()), + command.saveStatus(), + command.durability(), + command.executeAt(), + command.promised(), + command.acceptedOrCommitted(), + command.partialDeps() == null ? null : command.partialDeps().asListUnsafe(), + command.waitingOn() == null ? null : command.waitingOn().asListUnsafe(), + true); // Single transaction doesn't use property filtering + return Response.success(txnInfo); + } + } + catch (Exception e) + { + // Skip commands that can't be processed + continue; + } + } + + return Response.failure("Transaction " + txnIdStr + " not found in store " + storeId); + } + catch (Exception e) + { + return Response.failure("Failed to access transaction: " + e.getMessage()); + } + } + + public static Response getRedundantBefore(Node node, int storeId) + { + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + if (storeId < 0 || storeId >= allStores.length || allStores[storeId] == null) + { + return Response.failure("Store " + storeId + " not found"); + } + + CommandStore store = allStores[storeId]; + RedundantBefore redundantBefore = store.unsafeGetRedundantBefore(); + + return Response.success(Model.RedundantBeforeInfo.asJson(redundantBefore)); + } + catch (Exception e) + { + return Response.failure("Failed to access redundant before data: " + e.getMessage()); + } + } + + public static Response getCommandsForKey(Node node, int storeId, String txnIdStr) + { + try + { + CommandStores commandStores = node.commandStores(); + CommandStore[] allStores = commandStores.all(); + + if (storeId < 0 || storeId >= allStores.length || allStores[storeId] == null) + { + return Response.failure("Store " + storeId + " not found"); + } + + InMemoryCommandStore store = (InMemoryCommandStore) allStores[storeId]; + NavigableMap commands = store.unsafeCommands(); + + // First, find the transaction to get its routing key + RoutingKey routingKey = null; + for (GlobalCommand globalCommand : commands.values()) + { + try + { + Command command = globalCommand.value(); + if (command == null) continue; + + if (txnIdStr.equals(command.txnId().toString())) + { + routingKey = command.route().homeKey(); + break; + } + } + catch (Exception e) + { + continue; + } + } + + if (routingKey == null) + { + return Response.failure("Transaction " + txnIdStr + " not found in store " + storeId); + } + + // Now get CommandsForKey for this routing key + GlobalCommandsForKey globalCfk = store.commandsForKey(routingKey); + if (globalCfk == null) + { + return Response.failure("CommandsForKey not found for routing key " + routingKey); + } + + CommandsForKey cfk = globalCfk.value(); + if (cfk == null) + { + return Response.failure("CommandsForKey value is null for routing key " + routingKey); + } + + // Build the transaction info list + List txnInfos = new ArrayList<>(); + for (int i = 0; i < cfk.size(); ++i) + { + CommandsForKey.TxnInfo txn = cfk.get(i); + String plainTxnId = toStringOrNull(txn.plainTxnId()); + String ballot = toStringOrNull(txn.ballot()); + String depsKnownUntilExecuteAt = toStringOrNull(txn.depsKnownUntilExecuteAt()); + String flags = flags(txn); + String plainExecuteAt = toStringOrNull(txn.plainExecuteAt()); + String missing = java.util.Arrays.toString(txn.missing()); + String status = toStringOrNull(txn.status()); + String statusOverrides = txn.statusOverrides() == 0 ? null : ("0x" + Integer.toHexString(txn.statusOverrides())); + + txnInfos.add(new Model.CommandsForKeyTxnInfo(plainTxnId, ballot, depsKnownUntilExecuteAt, + flags, plainExecuteAt, missing, status, statusOverrides)); + } + + return Response.success(new Model.CommandsForKeyInfo(routingKey, txnInfos)); + } + catch (Exception e) + { + return Response.failure("Failed to access CommandsForKey: " + e.getMessage()); + } + } + + private static String toStringOrNull(Object obj) + { + return obj == null ? null : obj.toString(); + } + + private static String flags(CommandsForKey.TxnInfo txn) + { + StringBuilder sb = new StringBuilder(); + if (!txn.mayExecute()) + { + sb.append("NO EXECUTE"); + } + if (txn.hasNotifiedReady()) + { + if (sb.length() > 0) sb.append(", "); + sb.append("NOTIFIED READY"); + } + if (txn.hasNotifiedWaiting()) + { + if (sb.length() > 0) sb.append(", "); + sb.append("NOTIFIED WAITING"); + } + return sb.toString(); + } + + private static String safeToString(Object obj) + { + if (obj == null) return "null"; + try + { + return obj.toString(); + } + catch (Exception e) + { + return "Error: " + e.getMessage(); + } + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/Model.java b/accord-debug/src/main/java/accord/debug/Model.java new file mode 100644 index 0000000000..e2b2af8e06 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/Model.java @@ -0,0 +1,238 @@ +/* + * 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 accord.debug; + +import accord.api.RoutingKey; +import accord.local.RedundantStatus; +import accord.primitives.Ballot; +import accord.primitives.Range; +import accord.primitives.SaveStatus; +import accord.primitives.Status; +import accord.primitives.Timestamp; +import accord.primitives.TxnId; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Simplified representation of some of the entities in Accord protocol +public class Model +{ + public static class NodeInfo + { + public final int id; + public final List stores; + + public NodeInfo(int id, List stores) + { + this.id = id; + this.stores = stores; + } + } + + public static class StoreInfo + { + public final int storeId; + public final List ranges; + + public StoreInfo(int storeId, List ranges) + { + this.storeId = storeId; + this.ranges = ranges; + } + } + + public static class RedundantBeforeInfo + { + public static class MaxBounds + { + // Map of RedundantStatus$Property to TxnId + public final Map maxBounds; + + public MaxBounds() + { + this.maxBounds = new HashMap<>(); + } + + public void addProperty(RedundantStatus.Property property, TxnId maxBound) + { + if (maxBound != null && maxBound != TxnId.NONE) + { + maxBounds.put(property, maxBound); + } + } + } + + public static class Bounds + { + public final Range range; + public final long startEpoch; + public final long endEpoch; + public final MaxBounds maxBounds; + public final Timestamp staleUntilAtLeast; + public final TxnId bootstrappedAt; + public final TxnId gcBefore; + + public Bounds(Range range, long startEpoch, long endEpoch, Timestamp staleUntilAtLeast, MaxBounds maxBounds, TxnId bootstrappedAt, TxnId gcBefore) + { + this.range = range; + this.startEpoch = startEpoch; + this.endEpoch = endEpoch; + this.staleUntilAtLeast = staleUntilAtLeast; + this.maxBounds = maxBounds; + this.bootstrappedAt = bootstrappedAt; + this.gcBefore = gcBefore; + } + + public static Bounds fromBounds(accord.local.RedundantBefore.Bounds bounds) + { + MaxBounds maxBounds = new MaxBounds(); + + // Get max bounds for all properties + for (RedundantStatus.Property property : RedundantStatus.Property.values()) + { + TxnId maxBound = bounds.maxBound(property); + maxBounds.addProperty(property, maxBound); + } + + return new Bounds(bounds.range, bounds.startEpoch, bounds.endEpoch, bounds.staleUntilAtLeast, maxBounds, bounds.bootstrappedAt, bounds.gcBefore); + } + } + + public final Map ranges; + + public RedundantBeforeInfo(Map ranges) + { + this.ranges = ranges; + } + + + public static RedundantBeforeInfo asJson(accord.local.RedundantBefore redundantBefore) + { + if (redundantBefore == null || redundantBefore.isEmpty()) + return new RedundantBeforeInfo(Collections.emptyMap()); + + Map rangesMap = new HashMap<>(); + + // Use foldl to iterate through all ranges and bounds + redundantBefore.foldl((bounds, acc, p1, p2) -> { + if (bounds != null) + { + Bounds boundsJson = Bounds.fromBounds(bounds); + acc.put(boundsJson.range, boundsJson); + } + return acc; + }, rangesMap, null, null, ignore -> false); + + return new RedundantBeforeInfo(rangesMap); + } + + } + + public static class TxnInfo + { + public final TxnId txnId; + public final String routingKey; + public final String participants; + public final SaveStatus saveStatus; + public final Status.Durability durability; + public final Timestamp executeAt; + public final String promised; + public final String acceptedOrCommitted; + + public final List partialDeps; + public final List waitingOn; + public final boolean satisfiesProperty; + + public TxnInfo(TxnId txnId, RoutingKey routingKey, String participants, SaveStatus saveStatus, Status.Durability durability, + Timestamp executeAt, Ballot promised, Ballot acceptedOrCommitted, + List partialDeps, List waitingOn) + { + this.txnId = txnId; + this.routingKey = routingKey.toString(); + this.participants = participants; + this.saveStatus = saveStatus; + this.durability = durability; + this.executeAt = executeAt; + this.promised = promised == null ? null : promised.toStandardString(); + this.acceptedOrCommitted = acceptedOrCommitted == null ? null : acceptedOrCommitted.toStandardString(); + + this.partialDeps = partialDeps; + this.waitingOn = waitingOn; + this.satisfiesProperty = true; // Default to true for backward compatibility + } + + public TxnInfo(TxnId txnId, RoutingKey routingKey, String participants, SaveStatus saveStatus, Status.Durability durability, + Timestamp executeAt, Ballot promised, Ballot acceptedOrCommitted, + List partialDeps, List waitingOn, boolean satisfiesProperty) + { + this.txnId = txnId; + this.routingKey = routingKey == null ? null : routingKey.toString(); + this.participants = participants; + this.saveStatus = saveStatus; + this.durability = durability; + this.executeAt = executeAt; + this.promised = promised == null ? null : promised.toStandardString(); + this.acceptedOrCommitted = acceptedOrCommitted == null ? null : acceptedOrCommitted.toStandardString(); + + this.partialDeps = partialDeps; + this.waitingOn = waitingOn; + this.satisfiesProperty = satisfiesProperty; + } + } + + public static class CommandsForKeyInfo + { + public final String routingKey; + public final List transactions; + + public CommandsForKeyInfo(RoutingKey routingKey, List transactions) + { + this.routingKey = routingKey.toString(); + this.transactions = transactions; + } + } + + public static class CommandsForKeyTxnInfo + { + public final String plainTxnId; + public final String ballot; + public final String depsKnownUntilExecuteAt; + public final String flags; + public final String plainExecuteAt; + public final String missing; + public final String status; + public final String statusOverrides; + + public CommandsForKeyTxnInfo(String plainTxnId, String ballot, String depsKnownUntilExecuteAt, + String flags, String plainExecuteAt, String missing, + String status, String statusOverrides) + { + this.plainTxnId = plainTxnId; + this.ballot = ballot; + this.depsKnownUntilExecuteAt = depsKnownUntilExecuteAt; + this.flags = flags; + this.plainExecuteAt = plainExecuteAt; + this.missing = missing; + this.status = status; + this.statusOverrides = statusOverrides; + } + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/NewServer.java b/accord-debug/src/main/java/accord/debug/NewServer.java new file mode 100644 index 0000000000..6b15cff09d --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/NewServer.java @@ -0,0 +1,36 @@ +package accord.debug; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import accord.debug.controller.BurnTestController; +import accord.local.Node; + +public class NewServer extends AbstractServer +{ + private static final AtomicReference instance = new AtomicReference<>(); + + private BurnTestController controller; + + public NewServer(int port) + { + this(port, new BurnTestController(new ConcurrentHashMap<>())); + } + + private NewServer(int port, BurnTestController controller) + { + super(port, controller); + this.controller = controller; + instance.compareAndSet(null, this); + } + + public void registerNode(Node node) + { + controller.registerNode(node); + } + + public static AbstractServer getInstance() + { + return instance.get(); + } +} diff --git a/accord-debug/src/main/java/accord/debug/OldServer.java b/accord-debug/src/main/java/accord/debug/OldServer.java new file mode 100644 index 0000000000..890aa73ea5 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/OldServer.java @@ -0,0 +1,342 @@ +/* + * 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 accord.debug; + +import accord.local.Node; +import accord.local.RedundantStatus; +import accord.primitives.Range; +import accord.primitives.Timestamp; +import accord.primitives.TxnId; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import io.javalin.Javalin; +import io.javalin.http.Context; +import io.javalin.http.staticfiles.Location; +import io.javalin.json.JavalinGson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +public class OldServer +{ + private static final Logger logger = LoggerFactory.getLogger(OldServer.class); + + private static final AtomicReference instance = new AtomicReference<>(); + + private final Javalin app; + private final Map nodes = new ConcurrentHashMap<>(); + private final AtomicReference> debugFuture = new AtomicReference<>(); + + private final int port; + + public OldServer(int port) + { + this.port = port; + this.app = Javalin.create(config -> { +// config.staticFiles.add("/web", Location.CLASSPATH); + config.staticFiles.add("/Users/ifesdjeen/p/java/cassandra-accord-protocol/accord-debug/src/main/resources/web", Location.EXTERNAL); + config.jsonMapper(createCustomJsonMapper()); + }); + + setupRoutes(); + + // Register as singleton instance + instance.compareAndSet(null, this); + } + + private static JavalinGson createCustomJsonMapper() + { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Range.class, new ToStringSerializer()) + .registerTypeAdapter(TxnId.class, new ToStringSerializer()) + .registerTypeAdapter(Timestamp.class, new ToStringSerializer()) + .registerTypeAdapter(RedundantStatus.Property.class, new ToStringSerializer()) + .create(); + + return new JavalinGson(gson); + } + + private static class ToStringSerializer implements JsonSerializer + { + @Override + public JsonElement serialize(T src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) + { + if (src == null) + { + return null; + } + return new JsonPrimitive(src.toString()); + } + } + + public static OldServer getInstance() + { + return instance.get(); + } + + private void setupRoutes() + { + app.get("/nodes", this::handleNodes); + app.get("/node/{nodeId}/stores", this::handleStores); + app.get("/node/{nodeId}/stores/{storeId}/txns", this::handleTransactions); + app.get("/node/{nodeId}/stores/{storeId}/txn/{txnId}", this::handleSingleTransaction); + app.get("/node/{nodeId}/stores/{storeId}/txn/{txnId}/commands-for-key", this::handleCommandsForKey); + app.get("/node/{nodeId}/stores/{storeId}/redundant-before", this::handleRedundantBefore); + app.get("/unpause", this::handleUnpause); + } + + public void start() + { + app.start(port); + logger.info("Debug server started on port {}", app.port()); + } + + public void stop() + { + app.stop(); + logger.info("Debug server stopped"); + } + + public void registerNode(int nodeId, Node node) + { + nodes.put(nodeId, node); + logger.info("Registered node {} with debug server", nodeId); + } + + public void pause() + { + try + { + pauseInternal().get(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + private CompletableFuture pauseInternal() + { + while (true) + { + CompletableFuture future = debugFuture.get(); + if (future == null) + { + future = new CompletableFuture<>(); + if (debugFuture.compareAndSet(null, future)) + return future; + + future.cancel(true); + } + else + return future; + } + } + + private void handleStores(Context ctx) + { + try + { + int nodeId = Integer.parseInt(ctx.pathParam("nodeId")); + Node node = nodes.get(nodeId); + + if (node == null) + { + Response.sendResponse(ctx, Response.failure("Node " + nodeId + " not found"), 404); + return; + } + + Response> response = DeprecatedController.getStoresList(node); + Response.sendResponse(ctx, response); + } + catch (Exception e) + { + logger.error("Error handling stores request", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleTransactions(Context ctx) + { + try + { + int nodeId = Integer.parseInt(ctx.pathParam("nodeId")); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + String property = ctx.queryParam("property"); + + Node node = nodes.get(nodeId); + if (node == null) + { + Response.sendResponse(ctx, Response.failure("Node " + nodeId + " not found"), 404); + return; + } + + Response> response = DeprecatedController.getTransactionsList(node, storeId, property); + Response.sendResponse(ctx, response, 404); + } + catch (Exception e) + { + logger.error("Error handling transactions request", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleSingleTransaction(Context ctx) + { + try + { + int nodeId = Integer.parseInt(ctx.pathParam("nodeId")); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + String txnIdStr = ctx.pathParam("txnId"); + + Node node = nodes.get(nodeId); + if (node == null) + { + Response.sendResponse(ctx, Response.failure("Node " + nodeId + " not found"), 404); + return; + } + + Response response = DeprecatedController.getSingleTransaction(node, storeId, txnIdStr); + Response.sendResponse(ctx, response, 404); + } + catch (Exception e) + { + logger.error("Error handling single transaction request", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleCommandsForKey(Context ctx) + { + try + { + int nodeId = Integer.parseInt(ctx.pathParam("nodeId")); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + String txnIdStr = ctx.pathParam("txnId"); + + Node node = nodes.get(nodeId); + if (node == null) + { + Response.sendResponse(ctx, Response.failure("Node " + nodeId + " not found"), 404); + return; + } + + Response response = DeprecatedController.getCommandsForKey(node, storeId, txnIdStr); + Response.sendResponse(ctx, response, 404); + } + catch (Exception e) + { + logger.error("Error handling commands for key request", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleRedundantBefore(Context ctx) + { + try + { + int nodeId = Integer.parseInt(ctx.pathParam("nodeId")); + int storeId = Integer.parseInt(ctx.pathParam("storeId")); + + Node node = nodes.get(nodeId); + if (node == null) + { + Response.sendResponse(ctx, Response.failure("Node " + nodeId + " not found"), 404); + return; + } + + Response response = DeprecatedController.getRedundantBefore(node, storeId); + Response.sendResponse(ctx, response, 404); + } + catch (Exception e) + { + logger.error("Error handling redundant before request", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleNodes(Context ctx) + { + try + { + String rangeFilter = ctx.queryParam("range"); + + if (rangeFilter != null && !rangeFilter.trim().isEmpty()) + { + Response> response = DeprecatedController.getNodesByRange(nodes, rangeFilter.trim()); + Response.sendResponse(ctx, response); + } + else + { + Response> response = DeprecatedController.getNodesWithStores(nodes); + Response.sendResponse(ctx, response); + } + } + catch (Exception e) + { + logger.error("Error getting nodes", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + private void handleUnpause(Context ctx) + { + try + { + CompletableFuture future = debugFuture.getAndSet(null); + if (future != null) future.complete(null); + Response.sendResponse(ctx, Response.success("Debug session ended")); + logger.info("Debug session ended via /unpause endpoint"); + } + catch (Exception e) + { + logger.error("Error ending debug session", e); + Response.sendResponse(ctx, Response.failure("Internal server error: " + e.getMessage())); + } + } + + public static void main(String[] args) throws IOException + { + int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080; + OldServer server = new OldServer(port); + + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + + server.start(); + logger.info("Debug server running on http://localhost:{}", port); + logger.info("Web Interface: http://localhost:{}/", port); + logger.info("Available endpoints:"); + logger.info(" GET / - Web interface"); + logger.info(" GET /nodes - List all registered node IDs"); + logger.info(" GET /health - Server health status"); + logger.info(" GET /node/{id}/stores - List command stores for node"); + logger.info(" GET /node/{id}/stores/{storeId}/txns - List transactions for store"); + logger.info(" GET /unpause - End debug session (completes debug future)"); + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/Response.java b/accord-debug/src/main/java/accord/debug/Response.java new file mode 100644 index 0000000000..6ff8642609 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/Response.java @@ -0,0 +1,136 @@ +/* + * 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 accord.debug; + +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javalin.http.Context; + +public interface Response +{ + public static final Logger LOGGER = LoggerFactory.getLogger(Response.class); + public static Response compute(Supplier create) + { + try + { + return Response.success(create.get()); + } + catch (Throwable t) + { + LOGGER.error("Caught an error while computing value", t); + return failure(t.getMessage()); + } + } + boolean isSuccess(); + T getData(); + String getError(); + + static Response success(T data) + { + return new Success<>(data); + } + + static Response failure(String error) + { + return new Failure<>(error); + } + + static void sendResponse(Context ctx, Response response) + { + if (response.isSuccess()) + { + ctx.json(response); // TODO: this is not wired through for "debug" project + } + else + { + ctx.status(500).json(response); + } + } + + static void sendResponse(Context ctx, Response response, int errorStatus) + { + if (response.isSuccess()) + { + ctx.json(response); + } + else + { + ctx.status(errorStatus).json(response); + } + } + + class Success implements Response + { + public final T data; + + Success(T data) + { + this.data = data; + } + + @Override + public boolean isSuccess() + { + return true; + } + + @Override + public T getData() + { + return data; + } + + @Override + public String getError() + { + return null; + } + } + + class Failure implements Response + { + public final String error; + + Failure(String error) + { + this.error = error; + } + + @Override + public boolean isSuccess() + { + return false; + } + + @Override + public T getData() + { + return null; + } + + @Override + public String getError() + { + return error; + } + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/controller/BurnTestController.java b/accord-debug/src/main/java/accord/debug/controller/BurnTestController.java new file mode 100644 index 0000000000..376e341d03 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/controller/BurnTestController.java @@ -0,0 +1,659 @@ +/* + * 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 accord.debug.controller; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import accord.coordinate.Coordination; +import accord.debug.model.*; +import accord.debug.util.BlockedGraphUtil; +import accord.debug.util.CommandStoreTxnBlockedGraph; +import accord.impl.InMemoryCommandStore; +import accord.impl.progresslog.DefaultProgressLog; +import accord.impl.progresslog.TxnStateKind; +import accord.local.Command; +import accord.local.CommandStore; +import accord.local.DurableBefore; +import accord.local.Node; +import accord.local.RedundantBefore; +import accord.local.RedundantStatus; +import accord.local.StoreParticipants; +import accord.local.cfk.CommandsForKey; +import accord.local.durability.ShardDurability; +import accord.primitives.Range; +import accord.primitives.Ranges; +import accord.primitives.TxnId; +import accord.topology.TopologyManager; +import accord.utils.Invariants; + +import static accord.local.RedundantStatus.Property.GC_BEFORE; +import static accord.local.RedundantStatus.Property.LOCALLY_APPLIED; +import static accord.local.RedundantStatus.Property.LOCALLY_DURABLE_TO_COMMAND_STORE; +import static accord.local.RedundantStatus.Property.LOCALLY_DURABLE_TO_DATA_STORE; +import static accord.local.RedundantStatus.Property.LOCALLY_REDUNDANT; +import static accord.local.RedundantStatus.Property.LOCALLY_SYNCED; +import static accord.local.RedundantStatus.Property.LOCALLY_WITNESSED; +import static accord.local.RedundantStatus.Property.PRE_BOOTSTRAP; +import static accord.local.RedundantStatus.Property.QUORUM_APPLIED; +import static accord.local.RedundantStatus.Property.SHARD_APPLIED; + +public class BurnTestController implements Controller +{ + private final Map nodes; + + public BurnTestController(Map nodes) + { + this.nodes = nodes; + } + + public void registerNode(Node node) + { + Invariants.require(nodes.put(node.id().id, node) == null); + } + + @Override + public List getNodes() + { + List nodes = new ArrayList<>(); + this.nodes.forEach((i, n) -> { + List stores = new ArrayList<>(); + for (CommandStore store : n.commandStores().all()) + stores.add(new StoreInfo(store.id(), store.unsafeGetRangesForEpoch().currentRanges().toRanges())); + + nodes.add(new NodeInfo(Integer.toString(n.id().id), stores)); + }); + + return nodes; + } + + @Override + public List getRedundantBefore(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + List res = new ArrayList<>(); + for (CommandStore store : node.commandStores().all()) + { + // Use foldl to iterate through all ranges and bounds + store.unsafeGetRedundantBefore().foldl((entry, acc) -> { + acc.add(new RedundantBeforeInfo("n/a", "n/a", "n/a", + entry.range.start().toString(), + entry.range.end().toString(), + store.id(), + entry.startEpoch, + entry.endEpoch, + entry.maxBound(GC_BEFORE).toString(), + entry.maxBound(SHARD_APPLIED).toString(), + entry.maxBound(QUORUM_APPLIED).toString(), + entry.maxBound(LOCALLY_APPLIED).toString(), + entry.maxBound(LOCALLY_DURABLE_TO_COMMAND_STORE).toString(), + entry.maxBound(LOCALLY_DURABLE_TO_DATA_STORE).toString(), + entry.maxBound(LOCALLY_REDUNDANT).toString(), + entry.maxBound(LOCALLY_SYNCED).toString(), + entry.maxBound(LOCALLY_WITNESSED).toString(), + entry.maxBound(PRE_BOOTSTRAP).toString(), + entry.staleUntilAtLeast != null ? entry.staleUntilAtLeast.toString() : null)); + return acc; + }, res, ignore -> false); + } + return res; + } + + @Override + public List getCoordinations(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List coordinations = new ArrayList<>(); + + for (Coordination c : node.coordinations()) + { + coordinations.add(new CoordinationInfo( + toStringOrNull(c.txnId()), + c.kind().toString(), + c.coordinationId(), + c.describe(), + toStringOrNull(c.nodes()), + toStringOrNull(c.inflight()), + toStringOrNull(c.contacted()), + toStringOrNull(c.scope()), + summarise(c.replies()), + summarise(c.tracker()) + )); + } + + return coordinations; + } + + @Override + public List getTxn(String nodeId, String txnId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + TxnId parsedTxnId = TxnId.parse(txnId); + + // Use the same pattern as AccordDebugKeyspace.TxnTable + for (CommandStore s : node.commandStores().all()) + { + InMemoryCommandStore store = (InMemoryCommandStore) s; + InMemoryCommandStore.GlobalCommand command = store.command(parsedTxnId); + if (command.value() != null) + results.add(createTxnInfo(store.id(), txnId, command.value())); + } + + return results; + } + + private TxnInfo createTxnInfo(int commandStoreId, String txnIdStr, Command command) + { + System.out.println(command.waitingOn()); + return new TxnInfo(commandStoreId, + txnIdStr, + toStringOrNull(command.saveStatus()), + toStringOrNull(command.route()), + toStringOrNull(command.durability()), + toStringOrNull(command.executeAt()), + toStringOrNull(command.executesAtLeast()), + toStringOrNull(command.partialTxn()), + toStringOrNull(command.partialDeps()), + command.waitingOn() == null ? null : command.waitingOn().keys.stream().map(Object::toString).collect(Collectors.toList()), + command.waitingOn() == null ? null : command.waitingOn().asListUnsafe().stream().map(Object::toString).collect(Collectors.toList()), + toStringOrNull(command.writes()), + toStringOrNull(command.result()), + toStr(command.participants(), StoreParticipants::owns), + toStr(command.participants(), StoreParticipants::touches), + toStringOrNull(command.participants().hasTouched()), + toStr(command.participants(), StoreParticipants::executes), + toStr(command.participants(), StoreParticipants::waitsOn) + ); + } + + public List getTxnBlockedBy(String nodeId, String txnId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + TxnId parsedTxnId = TxnId.parse(txnId); + + BlockedGraphUtil util = new BlockedGraphUtil(); + List shards = util.loadDebug(node, parsedTxnId); + + for (CommandStoreTxnBlockedGraph shard : shards) + { + Set processed = new HashSet<>(); + process(results, shard, processed, parsedTxnId, 0, Integer.MAX_VALUE, parsedTxnId, "Self", null); + + // Verify everything was processed + if (!shard.txns.isEmpty() && !shard.txns.keySet().containsAll(processed)) + { + Set skipped = new HashSet<>(shard.txns.keySet()); + skipped.removeAll(processed); + // Log skipped transactions but don't fail - this is debug information + } + } + + return results; + } + + private void process(List results, CommandStoreTxnBlockedGraph shard, + Set processed, TxnId userTxn, int depth, int maxDepth, + TxnId txnId, String reason, Runnable onDone) + { + if (!processed.add(txnId)) + return; // Already processed + + CommandStoreTxnBlockedGraph.TxnState txn = shard.txns.get(txnId); + if (txn == null) + { + if (!"Self".equals(reason)) + return; // Unknown transaction + } + + // Skip applied transactions unless it's the root transaction + if (!"Self".equals(reason) && txn != null && txn.saveStatus.hasBeen(accord.primitives.Status.Applied)) + return; + + // Create TxnBlockedByInfo entry + results.add(new TxnBlockedByInfo( + userTxn.toString(), // txn_id + "n/a", // keyspace_name - would need table metadata + "n/a", // table_name - would need table metadata + shard.commandStoreId, // command_store_id + depth, // depth + "Self".equals(reason) ? "" : txn.txnId.toString(), // blocked_by + reason, // reason + txn != null ? txn.saveStatus.name() : null, // save_status + txn != null && txn.executeAt != null ? txn.executeAt.toString() : null, // execute_at + null // key (set by onDone for key-based blocking) + )); + + if (onDone != null) + onDone.run(); + + if (txn != null && txn.isBlocked() && depth < maxDepth) + { + // Process transactions this one is blocked by + for (TxnId blockedBy : txn.blockedBy) + { + if (!processed.contains(blockedBy)) + process(results, shard, processed, userTxn, depth + 1, maxDepth, blockedBy, "Txn", null); + } + + // Process keys this one is blocked by + for (accord.api.RoutingKey blockedBy : txn.blockedByKey) + { + TxnId blocking = shard.keys.get(blockedBy); + if (blocking != null && !processed.contains(blocking)) + { + process(results, shard, processed, userTxn, depth + 1, maxDepth, blocking, "Key", + () -> { + // Update the key field for the last added result + if (!results.isEmpty()) + { + TxnBlockedByInfo last = results.get(results.size() - 1); + // Create a new TxnBlockedByInfo with the key field set + TxnBlockedByInfo updated = new TxnBlockedByInfo( + last.txnId, last.keyspaceName, last.tableName, + last.commandStoreId, last.depth, last.blockedBy, + last.reason, last.saveStatus, last.executeAt, + blockedBy.toString() // key + ); + results.set(results.size() - 1, updated); + } + }); + } + } + } + } + + @Override + public List getProgressLog(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + + // Access progress log from command stores + node.commandStores().forEach((store, rangesForEpoch) -> { + try + { + DefaultProgressLog.ImmutableView view = ((DefaultProgressLog) store.unsafeProgressLog()).immutableView(); + while (view.advance()) + { + results.add(new ProgressLogInfo( + "n/a", // keyspace_name - would need table metadata access + "n/a", // table_name - would need table metadata access + "n/a", // table_id - would need table metadata access + view.commandStoreId(), // command_store_id + view.txnId().toString(), // txn_id + view.contactEveryone(), // contact_everyone + view.isWaitingUninitialised(), // waiting_is_uninitialised + view.waitingIsBlockedUntil().name(), // waiting_blocked_until + view.waitingHomeSatisfies().name(), // waiting_home_satisfies + view.waitingProgress().name(), // waiting_progress + view.waitingRetryCounter(), // waiting_retry_counter + Long.toBinaryString(view.waitingPackedKeyTrackerBits()), // waiting_packed_key_tracker_bits + toTimestamp(view.timerScheduledAt(TxnStateKind.Waiting)), // waiting_scheduled_at + view.homePhase().name(), // home_phase + view.homeProgress().name(), // home_progress + view.homeRetryCounter(), // home_retry_counter + toTimestamp(view.timerScheduledAt(TxnStateKind.Home)) // home_scheduled_at + )); + } + } + catch (Exception e) + { + // Progress log not accessible, continue + } + }); + + return results; + } + + private static long toTimestamp(Long deadline) + { + if (deadline == null) + return 0; + // Convert from microseconds to milliseconds (similar to AccordDebugKeyspace pattern) + return deadline / 1000L; + } + + @Override + public List getDurabilityService(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + + ShardDurability.ImmutableView view = node.durability().shards().immutableView(); + while (view.advance()) + { + results.add(new DurabilityServiceInfo( + "n/a", // keyspace_name - would need table metadata access + "n/a", // table_name - would need table metadata access + view.shard().range.start().toString(), // token_start + view.shard().range.end().toString(), // token_end + view.lastStartedAtMicros() * 1000, // last_started_at (convert to millis) + view.cycleStartedAtMicros() * 1000, // cycle_started_at (convert to millis) + view.retries(), // retries + toStringOrNull(view.min()), // min + toStringOrNull(view.requestedBy()), // requested_by + toStringOrNull(view.active()), // active + toStringOrNull(view.waiting()), // waiting + view.nodeOffset(), // node_offset + view.cycleOffset(), // cycle_offset + view.activeIndex(), // active_index + view.nextIndex(), // next_index + view.toIndex(), // next_to_index + view.cycleLength(), // end_index + view.currentSplits(), // current_splits + view.stopping(), // stopping + view.stopped() // stopped + )); + } + + return results; + } + + @Override + public List getCommandStores(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + List res = new ArrayList<>(); + node.commandStores().forEach((store, rangesForEpoch) -> { + Map> safeToReadMap = new LinkedHashMap<>(); + Map> rangesForEpochMap = new LinkedHashMap<>(); + rangesForEpoch.forEach((epoch, ranges) -> { + rangesForEpochMap.put(Long.toString(epoch), + toStrings(ranges)); + }); + + store.unsafeGetSafeToRead().forEach((timestamp, ranges) -> { + safeToReadMap.put(timestamp.toStandardString(), + toStrings(ranges)); + }); + res.add(new CommandStoreInfo(store.id(), + safeToReadMap, + rangesForEpochMap)); + }); + return res; + } + + @Override + public List getDurableBefore(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + + DurableBefore durableBefore = node.durableBefore(); + durableBefore.foldlWithBounds( + (entry, acc, start, end) -> { + acc.add(new DurableBeforeInfo("n/a", + "n/a", + start.toString(), + end.toString(), + entry.quorumBefore.toString(), + entry.universalBefore.toString() + )); + return acc; + }, + results, + ignore -> false + ); + + return results; + } + + @Override + public List getTopologies(String nodeId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List results = new ArrayList<>(); + + TopologyManager.EpochsSnapshot snapshot = node.topology().epochsSnapshot(); + for (TopologyManager.EpochsSnapshot.Epoch epoch : snapshot) + { + // Create EpochInfo from the epoch data + EpochInfo epochInfo = new EpochInfo( + epoch.epoch, + epoch.ready.metadata.value, + epoch.ready.coordinate.value, + epoch.ready.data.value, + epoch.ready.reads.value, + epoch.ready.reads == TopologyManager.EpochsSnapshot.ResultStatus.SUCCESS + ); + + // Create a single TableEpoch entry with all range types + List tableEpochs = List.of(new TableEpoch( + epoch.epoch, + "n/a", // keyspace_name + "n/a", // table_name + rangesToStrings(epoch.addedRanges), // added + rangesToStrings(epoch.removedRanges), // removed + rangesToStrings(epoch.synced), // synced + rangesToStrings(epoch.closed), // closed + rangesToStrings(epoch.retired) // retired + )); + + results.add(new TopologyInfo(epochInfo, tableEpochs)); + } + + return results; + } + + public List getCommandsForKey(String nodeId, String key) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + List res = new ArrayList<>(); + // In burn test environment, we need to work with available RoutingKey implementations + // The AccordDebugKeyspace uses TokenKey.parse() but we don't have that here + // For now, we'll create a framework for when proper key parsing becomes available + + for (CommandStore s : node.commandStores().all()) + { + InMemoryCommandStore store = (InMemoryCommandStore) s; + InMemoryCommandStore.GlobalCommandsForKey ref = store.commandsForKey(key); + if (ref.value() != null) + { + CommandsForKey cfk = ref.value(); + for (int i = 0; i < cfk.size(); ++i) + { + CommandsForKey.TxnInfo txn = cfk.get(i); + res.add(new CommandsForKeyInfo( + key, + store.id(), + toStringOrNull(txn.plainTxnId()), + toStringOrNull(txn.ballot()), + toStringOrNull(txn.depsKnownUntilExecuteAt()), + toStringOrNull(txn.plainExecuteAt()), + buildFlags(txn), + Arrays.toString(txn.missing()), + toStringOrNull(txn.status()), + txn.statusOverrides() == 0 ? null : ("0x" + Integer.toHexString(txn.statusOverrides())) + )); + } + } + } + + return res; + } + + @Override + public List getTransactions(String nodeId, int storeId, String propertyFilter) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + // For now only for BurnTest + InMemoryCommandStore store = (InMemoryCommandStore) node.commandStores().all()[storeId]; + + // Get redundant before data for property filtering + RedundantBefore redundantBefore = null; + RedundantStatus.Property property = null; + if (propertyFilter != null && !propertyFilter.trim().isEmpty()) + { + try + { + property = RedundantStatus.Property.valueOf(propertyFilter.trim()); + redundantBefore = store.unsafeGetRedundantBefore(); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException(String.format("Invalid property: %s. Valid properties: %s", propertyFilter, Arrays.toString(RedundantStatus.Property.values()))); + } + } + + List transactions = new ArrayList<>(); + NavigableMap commands = store.unsafeCommands(); + + for (InMemoryCommandStore.GlobalCommand globalCommand : commands.values()) + { + Command command = globalCommand.value(); + if (command == null) + continue; + + // Apply property filter if specified + boolean satisfiesProperty = true; + if (property != null && redundantBefore != null) + satisfiesProperty = RedundantBefore.satisfies(redundantBefore, command.txnId(), command.route().homeKey(), property); + + if (satisfiesProperty) + { + TxnInfo txnInfo = createTxnInfo(store.id(), command.txnId().toString(), command); + transactions.add(txnInfo); + } + } + return transactions; + } + + @Override + public TxnInfo getTransaction(String nodeId, int storeId, String txnId) + { + Node node = nodes.get(Integer.parseInt(nodeId)); + Invariants.nonNull(node); + + // For now only for BurnTest + InMemoryCommandStore store = (InMemoryCommandStore) node.commandStores().all()[storeId]; + TxnId parsedTxnId = TxnId.parse(txnId); + InMemoryCommandStore.GlobalCommand command = store.unsafeCommands().get(parsedTxnId); + if (command.value() == null) + return null; + + return createTxnInfo(store.id(), txnId, command.value()); + } + + private static String buildFlags(CommandsForKey.TxnInfo txn) + { + StringBuilder sb = new StringBuilder(); + if (!txn.mayExecute()) + { + sb.append("NO EXECUTE"); + } + if (txn.hasNotifiedReady()) + { + if (sb.length() > 0) sb.append(", "); + sb.append("NOTIFIED READY"); + } + if (txn.hasNotifiedWaiting()) + { + if (sb.length() > 0) sb.append(", "); + sb.append("NOTIFIED WAITING"); + } + return sb.toString(); + } + + private static String safeToString(Object obj) + { + if (obj == null) return "null"; + try + { + return obj.toString(); + } + catch (Exception e) + { + return "Error: " + e.getMessage(); + } + } + + private static List rangesToStrings(Ranges ranges) + { + List result = new ArrayList<>(); + for (Range range : ranges) + { + result.add(range.toString()); + } + return result; + } + + private static List toStrings(Ranges ranges) + { + List rangeStrings = new ArrayList<>(); + for (Range range : ranges) + rangeStrings.add(range.toString()); + return rangeStrings; + } + + private static String toStringOrNull(Object obj) + { + return obj == null ? null : obj.toString(); + } + + private static String summarise(Object obj) + { + // TODO: Implement proper summarization logic based on the object type + // This should provide a concise summary of complex objects like replies and trackers + return obj == null ? null : obj.toString(); + } + + private static String toStr(StoreParticipants participants, Function extractor) + { + if (participants == null) return null; + try + { + Object result = extractor.apply(participants); + return toStringOrNull(result); + } + catch (Exception e) + { + return null; + } + } +} diff --git a/accord-debug/src/main/java/accord/debug/controller/Controller.java b/accord-debug/src/main/java/accord/debug/controller/Controller.java new file mode 100644 index 0000000000..b49d8c6e19 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/controller/Controller.java @@ -0,0 +1,41 @@ +/* + * 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 accord.debug.controller; + +import java.util.List; + +import accord.debug.model.*; + +public interface Controller +{ + public List getNodes(); + public List getRedundantBefore(String nodeId); + public List getCoordinations(String nodeId); + public List getTxn(String nodeId, String txnId); + public List getTxnBlockedBy(String nodeId, String txnId); + public List getProgressLog(String nodeId); + public List getDurabilityService(String nodeId); + public List getCommandStores(String nodeId); + public List getDurableBefore(String nodeId); + public List getTopologies(String nodeId); + public List getTransactions(String nodeId, int commandStoreId, String propertyFilter); + public List getCommandsForKey(String nodeId, String key); + public TxnInfo getTransaction(String nodeId, int commandStoreId, String txnId); + // TODO: CommandsForKeys +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/CommandStoreInfo.java b/accord-debug/src/main/java/accord/debug/model/CommandStoreInfo.java new file mode 100644 index 0000000000..546a490d2e --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/CommandStoreInfo.java @@ -0,0 +1,21 @@ +package accord.debug.model; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class CommandStoreInfo +{ + public final int commandStoreId; + public final Map> safeToRead; + public final Map> rangesForEpoch; + + public CommandStoreInfo(int commandStoreId, + Map> safeToRead, + Map> rangesForEpoch) + { + this.commandStoreId = commandStoreId; + this.safeToRead = safeToRead != null ? safeToRead : Collections.emptyMap(); + this.rangesForEpoch = rangesForEpoch != null ? rangesForEpoch : Collections.emptyMap(); + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/CommandsForKeyInfo.java b/accord-debug/src/main/java/accord/debug/model/CommandsForKeyInfo.java new file mode 100644 index 0000000000..57adf1dd33 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/CommandsForKeyInfo.java @@ -0,0 +1,49 @@ +/* + * 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 accord.debug.model; + +public class CommandsForKeyInfo +{ + public final String key; + public final int commandStoreId; + public final String txnId; + public final String ballot; + public final String depsKnownBefore; + public final String executeAt; + public final String flags; + public final String missing; + public final String status; + public final String statusOverrides; + + public CommandsForKeyInfo(String key, int commandStoreId, String txnId, String ballot, + String depsKnownBefore, String executeAt, String flags, String missing, + String status, String statusOverrides) + { + this.key = key; + this.commandStoreId = commandStoreId; + this.txnId = txnId; + this.ballot = ballot; + this.depsKnownBefore = depsKnownBefore; + this.executeAt = executeAt; + this.flags = flags; + this.missing = missing; + this.status = status; + this.statusOverrides = statusOverrides; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/CoordinationInfo.java b/accord-debug/src/main/java/accord/debug/model/CoordinationInfo.java new file mode 100644 index 0000000000..3ec9222a75 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/CoordinationInfo.java @@ -0,0 +1,94 @@ +/* + * 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 accord.debug.model; + +import java.util.ArrayList; +import java.util.List; + +import accord.local.Node; + +public class CoordinationInfo +{ + public final String txnId; + public final String kind; + public final long coordinationId; + public final String description; + public final String nodes; + public final String nodesInflight; + public final String nodesContacted; + public final String participants; + public final String replies; + public final String tracker; + + public CoordinationInfo(String txnId, String kind, long coordinationId, String description, String nodes, + String nodesInflight, String nodesContacted, String participants, String replies, String tracker) + { + this.txnId = txnId; + this.kind = kind; + this.coordinationId = coordinationId; + this.description = description; + this.nodes = nodes; + this.nodesInflight = nodesInflight; + this.nodesContacted = nodesContacted; + this.participants = participants; + this.replies = replies; + this.tracker = tracker; + } + + public static List getCoordinations(Node node) + { + List coordinations = new ArrayList<>(); + + // Note: The Coordination interface/class referenced in the Cassandra implementation + // may not be available in the current Accord codebase. This method provides a + // framework for when the Coordination API becomes available. + + // TODO: Once the Coordination API is available, implement the following pattern: + // + // Coordinations nodeCoordinations = node.coordinations(); + // for (Coordination c : nodeCoordinations) + // { + // coordinations.add(new CoordinationInfo( + // toStringOrNull(c.txnId()), + // c.kind().toString(), + // c.coordinationId(), + // c.describe(), + // toStringOrNull(c.nodes()), + // toStringOrNull(c.inflight()), + // toStringOrNull(c.contacted()), + // toStringOrNull(c.scope()), + // summarise(c.replies()), + // summarise(c.tracker()) + // )); + // } + + return coordinations; + } + + private static String toStringOrNull(Object obj) + { + return obj == null ? null : obj.toString(); + } + + private static String summarise(Object obj) + { + // TODO: Implement proper summarization logic based on the object type + return obj == null ? null : obj.toString(); + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/DebugServerConfig.java b/accord-debug/src/main/java/accord/debug/model/DebugServerConfig.java new file mode 100644 index 0000000000..9f543b618e --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/DebugServerConfig.java @@ -0,0 +1,82 @@ +/* + * 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 accord.debug.model; + +import java.util.List; + +public class DebugServerConfig +{ + private List hosts; + private ServerConfig server; + + public DebugServerConfig(List hosts, ServerConfig server) + { + this.hosts = hosts; + this.server = server; + } + + public List getHosts() + { + return hosts; + } + + public void setHosts(List hosts) + { + this.hosts = hosts; + } + + public ServerConfig getServer() + { + return server; + } + + public void setServer(ServerConfig server) + { + this.server = server; + } + + public static class HostConfig + { + public final String host; + public final int port; + + public HostConfig(String host, int port) + { + this.host = host; + this.port = port; + } + + public String toString() + { + return String.format("%s:%d", host, port); + } + } + + public static class ServerConfig + { + public final int port; + public final String host; + + public ServerConfig(int port, String host) + { + this.port = port; + this.host = host; + } + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/DurabilityServiceInfo.java b/accord-debug/src/main/java/accord/debug/model/DurabilityServiceInfo.java new file mode 100644 index 0000000000..b0f1f0b2e9 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/DurabilityServiceInfo.java @@ -0,0 +1,53 @@ +package accord.debug.model; + +public class DurabilityServiceInfo +{ + public final String keyspaceName; + public final String tableName; + public final String tokenStart; + public final String tokenEnd; + public final long lastStartedAt; + public final long cycleStartedAt; + public final int retries; + public final String min; + public final String requestedBy; + public final String active; + public final String waiting; + public final int nodeOffset; + public final int cycleOffset; + public final int activeIndex; + public final int nextIndex; + public final int nextToIndex; + public final int endIndex; + public final int currentSplits; + public final boolean stopping; + public final boolean stopped; + + public DurabilityServiceInfo(String keyspaceName, String tableName, String tokenStart, String tokenEnd, + long lastStartedAt, long cycleStartedAt, int retries, String min, String requestedBy, + String active, String waiting, int nodeOffset, int cycleOffset, int activeIndex, + int nextIndex, int nextToIndex, int endIndex, int currentSplits, boolean stopping, + boolean stopped) + { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.tokenStart = tokenStart; + this.tokenEnd = tokenEnd; + this.lastStartedAt = lastStartedAt; + this.cycleStartedAt = cycleStartedAt; + this.retries = retries; + this.min = min; + this.requestedBy = requestedBy; + this.active = active; + this.waiting = waiting; + this.nodeOffset = nodeOffset; + this.cycleOffset = cycleOffset; + this.activeIndex = activeIndex; + this.nextIndex = nextIndex; + this.nextToIndex = nextToIndex; + this.endIndex = endIndex; + this.currentSplits = currentSplits; + this.stopping = stopping; + this.stopped = stopped; + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/DurableBeforeInfo.java b/accord-debug/src/main/java/accord/debug/model/DurableBeforeInfo.java new file mode 100644 index 0000000000..5748fd332c --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/DurableBeforeInfo.java @@ -0,0 +1,22 @@ +package accord.debug.model; + +public class DurableBeforeInfo +{ + public final String keyspaceName; + public final String tableName; + public final String tokenStart; + public final String tokenEnd; + public final String quorum; + public final String universal; + + public DurableBeforeInfo(String keyspaceName, String tableName, String tokenStart, String tokenEnd, + String quorum, String universal) + { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.tokenStart = tokenStart; + this.tokenEnd = tokenEnd; + this.quorum = quorum; + this.universal = universal; + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/EpochInfo.java b/accord-debug/src/main/java/accord/debug/model/EpochInfo.java new file mode 100644 index 0000000000..0ba372f52d --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/EpochInfo.java @@ -0,0 +1,22 @@ +package accord.debug.model; + +public class EpochInfo +{ + public final long epoch; + public final String readyMetadata; + public final String readyCoordinate; + public final String readyData; + public final String readyReads; + public final boolean ready; + + public EpochInfo(long epoch, String readyMetadata, String readyCoordinate, + String readyData, String readyReads, boolean ready) + { + this.epoch = epoch; + this.readyMetadata = readyMetadata; + this.readyCoordinate = readyCoordinate; + this.readyData = readyData; + this.readyReads = readyReads; + this.ready = ready; + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/NodeInfo.java b/accord-debug/src/main/java/accord/debug/model/NodeInfo.java new file mode 100644 index 0000000000..c4d99fecd7 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/NodeInfo.java @@ -0,0 +1,33 @@ +/* + * 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 accord.debug.model; + +import java.util.List; + +public class NodeInfo +{ + public final String id; + public final List stores; + + public NodeInfo(String id, List stores) + { + this.id = id; + this.stores = stores; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/ProgressLogInfo.java b/accord-debug/src/main/java/accord/debug/model/ProgressLogInfo.java new file mode 100644 index 0000000000..e7ceae4c97 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/ProgressLogInfo.java @@ -0,0 +1,65 @@ +/* + * 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 accord.debug.model; + +public class ProgressLogInfo +{ + public final String keyspaceName; + public final String tableName; + public final String tableId; + public final int commandStoreId; + public final String txnId; + public final boolean contactEveryone; + public final boolean waitingIsUninitialised; + public final String waitingBlockedUntil; + public final String waitingHomeSatisfies; + public final String waitingProgress; + public final int waitingRetryCounter; + public final String waitingPackedKeyTrackerBits; + public final long waitingScheduledAt; + public final String homePhase; + public final String homeProgress; + public final int homeRetryCounter; + public final long homeScheduledAt; + + public ProgressLogInfo(String keyspaceName, String tableName, String tableId, int commandStoreId, String txnId, + boolean contactEveryone, boolean waitingIsUninitialised, String waitingBlockedUntil, + String waitingHomeSatisfies, String waitingProgress, int waitingRetryCounter, + String waitingPackedKeyTrackerBits, long waitingScheduledAt, String homePhase, + String homeProgress, int homeRetryCounter, long homeScheduledAt) + { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.tableId = tableId; + this.commandStoreId = commandStoreId; + this.txnId = txnId; + this.contactEveryone = contactEveryone; + this.waitingIsUninitialised = waitingIsUninitialised; + this.waitingBlockedUntil = waitingBlockedUntil; + this.waitingHomeSatisfies = waitingHomeSatisfies; + this.waitingProgress = waitingProgress; + this.waitingRetryCounter = waitingRetryCounter; + this.waitingPackedKeyTrackerBits = waitingPackedKeyTrackerBits; + this.waitingScheduledAt = waitingScheduledAt; + this.homePhase = homePhase; + this.homeProgress = homeProgress; + this.homeRetryCounter = homeRetryCounter; + this.homeScheduledAt = homeScheduledAt; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/RedundantBeforeInfo.java b/accord-debug/src/main/java/accord/debug/model/RedundantBeforeInfo.java new file mode 100644 index 0000000000..1378db95c6 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/RedundantBeforeInfo.java @@ -0,0 +1,70 @@ +/* + * 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 accord.debug.model; + +public class RedundantBeforeInfo +{ + public final String keyspaceName; + public final String tableName; + public final String tableId; + // TODO: group by token start / end in the UI + public final String tokenStart; + public final String tokenEnd; + public final int commandStoreId; + public final long startEpoch; + public final long endEpoch; + public final String gcBefore; + public final String shardApplied; + public final String quorumApplied; + public final String locallyApplied; + public final String locallyDurableToCommandStore; + public final String locallyDurableToDataStore; + public final String locallyRedundant; + public final String locallySynced; + public final String locallyWitnessed; + public final String preBootstrap; + public final String staleUntilAtLeast; + + public RedundantBeforeInfo(String keyspaceName, String tableName, String tableId, String tokenStart, String tokenEnd, + int commandStoreId, long startEpoch, long endEpoch, String gcBefore, String shardApplied, + String quorumApplied, String locallyApplied, String locallyDurableToCommandStore, + String locallyDurableToDataStore, String locallyRedundant, String locallySynced, + String locallyWitnessed, String preBootstrap, String staleUntilAtLeast) + { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.tableId = tableId; + this.tokenStart = tokenStart; + this.tokenEnd = tokenEnd; + this.commandStoreId = commandStoreId; + this.startEpoch = startEpoch; + this.endEpoch = endEpoch; + this.gcBefore = gcBefore; + this.shardApplied = shardApplied; + this.quorumApplied = quorumApplied; + this.locallyApplied = locallyApplied; + this.locallyDurableToCommandStore = locallyDurableToCommandStore; + this.locallyDurableToDataStore = locallyDurableToDataStore; + this.locallyRedundant = locallyRedundant; + this.locallySynced = locallySynced; + this.locallyWitnessed = locallyWitnessed; + this.preBootstrap = preBootstrap; + this.staleUntilAtLeast = staleUntilAtLeast; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/StoreInfo.java b/accord-debug/src/main/java/accord/debug/model/StoreInfo.java new file mode 100644 index 0000000000..1a5df5d9d8 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/StoreInfo.java @@ -0,0 +1,37 @@ +/* + * 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 accord.debug.model; + +import java.util.List; +import java.util.stream.Collectors; + +import accord.primitives.Range; +import accord.primitives.Ranges; + +public class StoreInfo +{ + public final int storeId; + public final List ranges; + + public StoreInfo(int storeId, Ranges ranges) + { + this.storeId = storeId; + this.ranges = ranges.stream().map(Range::toString).collect(Collectors.toList()); + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/TableEpoch.java b/accord-debug/src/main/java/accord/debug/model/TableEpoch.java new file mode 100644 index 0000000000..563ef1262a --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/TableEpoch.java @@ -0,0 +1,30 @@ +package accord.debug.model; + +import java.util.ArrayList; +import java.util.List; + +public class TableEpoch +{ + public final long epoch; + public final String keyspaceName; + public final String tableName; + public final List added; + public final List removed; + public final List synced; + public final List closed; + public final List retired; + + public TableEpoch(long epoch, String keyspaceName, String tableName, + List added, List removed, List synced, + List closed, List retired) + { + this.epoch = epoch; + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.added = added != null ? added : new ArrayList<>(); + this.removed = removed != null ? removed : new ArrayList<>(); + this.synced = synced != null ? synced : new ArrayList<>(); + this.closed = closed != null ? closed : new ArrayList<>(); + this.retired = retired != null ? retired : new ArrayList<>(); + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/TopologyInfo.java b/accord-debug/src/main/java/accord/debug/model/TopologyInfo.java new file mode 100644 index 0000000000..ca80dd1c8e --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/TopologyInfo.java @@ -0,0 +1,15 @@ +package accord.debug.model; + +import java.util.List; + +public class TopologyInfo +{ + public final EpochInfo epoch; + public final List tableEpochs; + + public TopologyInfo(EpochInfo epoch, List tableEpochs) + { + this.epoch = epoch; + this.tableEpochs = tableEpochs; + } +} diff --git a/accord-debug/src/main/java/accord/debug/model/TxnBlockedByInfo.java b/accord-debug/src/main/java/accord/debug/model/TxnBlockedByInfo.java new file mode 100644 index 0000000000..f89fdbcc9f --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/TxnBlockedByInfo.java @@ -0,0 +1,48 @@ +/* + * 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 accord.debug.model; + +public class TxnBlockedByInfo +{ + public final String txnId; + public final String keyspaceName; + public final String tableName; + public final int commandStoreId; + public final int depth; + public final String blockedBy; + public final String reason; + public final String saveStatus; + public final String executeAt; + public final String key; + + public TxnBlockedByInfo(String txnId, String keyspaceName, String tableName, int commandStoreId, int depth, + String blockedBy, String reason, String saveStatus, String executeAt, String key) + { + this.txnId = txnId; + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.commandStoreId = commandStoreId; + this.depth = depth; + this.blockedBy = blockedBy; + this.reason = reason; + this.saveStatus = saveStatus; + this.executeAt = executeAt; + this.key = key; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/model/TxnInfo.java b/accord-debug/src/main/java/accord/debug/model/TxnInfo.java new file mode 100644 index 0000000000..de9cfa250a --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/model/TxnInfo.java @@ -0,0 +1,69 @@ +/* + * 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 accord.debug.model; + +import java.util.List; + +public class TxnInfo +{ + public final int commandStoreId; + public final String txnId; + public final String saveStatus; + public final String route; + public final String durability; + public final String executeAt; + public final String executesAtLeast; + public final String txn; + public final String deps; + public final List waitingOnKeys; + public final List waitingOnTxnIds; + public final String writes; + public final String result; + public final String participantsOwns; + public final String participantsTouches; + public final String participantsHasTouched; + public final String participantsExecutes; + public final String participantsWaitsOn; + + public TxnInfo(int commandStoreId, String txnId, String saveStatus, String route, String durability, + String executeAt, String executesAtLeast, String txn, String deps, + List waitingOnKeys, List waitingOnTxnIds, + String writes, String result, String participantsOwns, String participantsTouches, + String participantsHasTouched, String participantsExecutes, String participantsWaitsOn) + { + this.commandStoreId = commandStoreId; + this.txnId = txnId; + this.saveStatus = saveStatus; + this.route = route; + this.durability = durability; + this.executeAt = executeAt; + this.executesAtLeast = executesAtLeast; + this.txn = txn; + this.deps = deps; + this.waitingOnKeys = waitingOnKeys; + this.waitingOnTxnIds = waitingOnTxnIds; + this.writes = writes; + this.result = result; + this.participantsOwns = participantsOwns; + this.participantsTouches = participantsTouches; + this.participantsHasTouched = participantsHasTouched; + this.participantsExecutes = participantsExecutes; + this.participantsWaitsOn = participantsWaitsOn; + } +} \ No newline at end of file diff --git a/accord-debug/src/main/java/accord/debug/util/BlockedGraphUtil.java b/accord-debug/src/main/java/accord/debug/util/BlockedGraphUtil.java new file mode 100644 index 0000000000..be3de971e2 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/util/BlockedGraphUtil.java @@ -0,0 +1,126 @@ +package accord.debug.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import accord.api.RoutingKey; +import accord.impl.InMemoryCommandStore; +import accord.impl.InMemorySafeCommand; +import accord.local.Command; +import accord.local.CommandStores; +import accord.local.Node; +import accord.local.cfk.CommandsForKey; +import accord.primitives.SaveStatus; +import accord.primitives.Status; +import accord.primitives.Timestamp; +import accord.primitives.TxnId; +import accord.utils.Invariants; + +public class BlockedGraphUtil +{ + public List loadDebug(Node node, TxnId original) + { + CommandStores commandStores = node.commandStores(); + if (commandStores.count() == 0) + return Collections.emptyList(); + int[] ids = commandStores.ids(); + List res = new ArrayList<>(ids.length); + for (int id : ids) + res.add(loadDebug(original, (InMemoryCommandStore) commandStores.forId(id))); + return res; + } + + private CommandStoreTxnBlockedGraph loadDebug(TxnId txnId, InMemoryCommandStore store) + { + CommandStoreTxnBlockedGraph.Builder state = new CommandStoreTxnBlockedGraph.Builder(store.id()); + populateSync(state, store, txnId); + return state.build(); + } + + private static void populate(CommandStoreTxnBlockedGraph.Builder state, InMemoryCommandStore safeStore, TxnId blockedBy) + { + populateSync(state, safeStore, blockedBy); + } + + private static void populateSync(CommandStoreTxnBlockedGraph.Builder state, InMemoryCommandStore store, TxnId txnId) + { + try + { + if (state.txns.containsKey(txnId)) + return; // could plausibly request same txn twice + + InMemorySafeCommand command = store.command(txnId).createSafeReference(); + Invariants.nonNull(command, "Txn %s is not in the cache", txnId); + if (command.current() == null || command.current().saveStatus() == SaveStatus.Uninitialised) + return; + + CommandStoreTxnBlockedGraph.TxnState cmdTxnState = populateSync(state, command.current()); + if (cmdTxnState.notBlocked()) + return; + + for (TxnId blockedBy : cmdTxnState.blockedBy) + { + if (!state.knows(blockedBy)) + populate(state, store, blockedBy); + } + for (RoutingKey blockedBy : cmdTxnState.blockedByKey) + { + if (!state.keys.containsKey(blockedBy)) + populate(state, store, blockedBy, txnId, command.current().executeAt()); + } + } + catch (Throwable t) + { + state.tryFailure(t); + } + } + + private static void populate(CommandStoreTxnBlockedGraph.Builder state, InMemoryCommandStore safeStore, RoutingKey blockedBy, TxnId txnId, Timestamp executeAt) + { + populateSync(state, safeStore, blockedBy, txnId, executeAt); + } + + private static void populateSync(CommandStoreTxnBlockedGraph.Builder state, InMemoryCommandStore store, RoutingKey pk, TxnId txnId, Timestamp executeAt) + { + try + { + InMemoryCommandStore.GlobalCommandsForKey commandsForKey = store.commandsForKey(pk); + TxnId blocking = commandsForKey.value().blockedOnTxnId(txnId, executeAt); + if (blocking instanceof CommandsForKey.TxnInfo) + blocking = ((CommandsForKey.TxnInfo) blocking).plainTxnId(); + state.keys.put(pk, blocking); + if (state.txns.containsKey(blocking)) + return; + populate(state, store, blocking); + } + catch (Throwable t) + { + state.tryFailure(t); + } + } + + private static CommandStoreTxnBlockedGraph.TxnState populateSync(CommandStoreTxnBlockedGraph.Builder state, Command cmd) + { + CommandStoreTxnBlockedGraph.Builder.TxnBuilder cmdTxnState = state.txn(cmd.txnId(), cmd.executeAt(), cmd.saveStatus()); + if (!cmd.hasBeen(Status.Applied) && cmd.hasBeen(Status.Stable)) + { + // check blocking state + Command.WaitingOn waitingOn = cmd.asCommitted().waitingOn(); + waitingOn.waitingOn.reverseForEach(null, null, null, null, (i1, i2, i3, i4, i) -> { + if (i < waitingOn.txnIdCount()) + { + // blocked on txn + cmdTxnState.blockedBy.add(waitingOn.txnId(i)); + } + else + { + // blocked on key + cmdTxnState.blockedByKey.add(waitingOn.keys.get(i - waitingOn.txnIdCount())); + } + }); + } + return cmdTxnState.build(); + } + +} diff --git a/accord-debug/src/main/java/accord/debug/util/CommandStoreTxnBlockedGraph.java b/accord-debug/src/main/java/accord/debug/util/CommandStoreTxnBlockedGraph.java new file mode 100644 index 0000000000..6e7f3f7f57 --- /dev/null +++ b/accord-debug/src/main/java/accord/debug/util/CommandStoreTxnBlockedGraph.java @@ -0,0 +1,134 @@ +/* + * 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 accord.debug.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import accord.api.RoutingKey; + +import accord.primitives.SaveStatus; +import accord.primitives.Timestamp; +import accord.primitives.TxnId; +import accord.utils.async.AsyncResults; + +public class CommandStoreTxnBlockedGraph +{ + public final int commandStoreId; + public final Map txns; + public final Map keys; + + public CommandStoreTxnBlockedGraph(Builder builder) + { + commandStoreId = builder.storeId; + txns = new HashMap<>(builder.txns); + keys = new HashMap<>(builder.keys); + } + + public static class TxnState + { + public final TxnId txnId; + public final Timestamp executeAt; + public final SaveStatus saveStatus; + public final List blockedBy; + public final Set blockedByKey; + + public TxnState(Builder.TxnBuilder builder) + { + txnId = builder.txnId; + executeAt = builder.executeAt; + saveStatus = builder.saveStatus; + blockedBy = new ArrayList<>(builder.blockedBy); + blockedByKey = new HashSet<>(builder.blockedByKey); + } + + public boolean isBlocked() + { + return !notBlocked(); + } + + public boolean notBlocked() + { + return blockedBy.isEmpty() && blockedByKey.isEmpty(); + } + } + + public static class Builder extends AsyncResults.SettableResult + { + final AtomicInteger asyncTxns = new AtomicInteger(), asyncKeys = new AtomicInteger(); + final int storeId; + final Map txns = new LinkedHashMap<>(); + final Map keys = new LinkedHashMap<>(); + + public Builder(int storeId) + { + this.storeId = storeId; + } + + boolean knows(TxnId id) + { + return txns.containsKey(id); + } + + public void complete() + { + trySuccess(build()); + } + + public CommandStoreTxnBlockedGraph build() + { + return new CommandStoreTxnBlockedGraph(this); + } + + public TxnBuilder txn(TxnId txnId, Timestamp executeAt, SaveStatus saveStatus) + { + return new TxnBuilder(txnId, executeAt, saveStatus); + } + + public class TxnBuilder + { + final TxnId txnId; + final Timestamp executeAt; + final SaveStatus saveStatus; + List blockedBy = new ArrayList<>(); + Set blockedByKey = new LinkedHashSet<>(); + + public TxnBuilder(TxnId txnId, Timestamp executeAt, SaveStatus saveStatus) + { + this.txnId = txnId; + this.executeAt = executeAt; + this.saveStatus = saveStatus; + } + + public TxnState build() + { + TxnState state = new TxnState(this); + txns.put(txnId, state); + return state; + } + } + } +} diff --git a/accord-debug/src/main/resources/web/cluster.html b/accord-debug/src/main/resources/web/cluster.html new file mode 100644 index 0000000000..2e53d8fe90 --- /dev/null +++ b/accord-debug/src/main/resources/web/cluster.html @@ -0,0 +1,238 @@ + + + + + + + Accord Debug Interface + + + + + + + +
+
+ {{ error }} +
+ +
+ +
+ +
+
+ Discovering nodes... +
+ +
+
+

Node {{ node.id }}

+ {{ node.stores.length }} stores +
+ +
+
+
+

Store {{ store.storeId }}

+
+ + {{ range }} + +
+
+ +
+
Scroll to load transactions...
+
Loading transactions...
+
{{ store.error }}
+
No transactions
+
+
+
{{ txn.txnId }}
+
{{ txn.saveStatus }}
+
+
+ No transactions match "{{ searchFilter }}" +
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/cluster.js b/accord-debug/src/main/resources/web/cluster.js new file mode 100644 index 0000000000..9b47fdb668 --- /dev/null +++ b/accord-debug/src/main/resources/web/cluster.js @@ -0,0 +1,395 @@ +/* + * 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. + */ + +const { createApp } = Vue; + +createApp({ + components: { + 'transaction-widget': window.TransactionWidgetComponent, + 'navigation-widget': window.NavigationWidgetComponent, + 'host-selector': window.HostSelectorComponent + }, + data() { + return { + nodes: [], + healthStatus: null, + loading: false, + error: null, + intersectionObserver: null, + searchFilter: '', + selectedRange: null, + selectedTransaction: null, + showTransactionPopup: false, + showCommandsForKeyPopup: false, + currentNodeId: null, + currentStoreId: null, + transactionBreadcrumbs: [], + commandsForKeyData: null + }; + }, + mounted() { + this.init(); + // Add keyboard event listener for escape key + document.addEventListener('keydown', this.handleKeyDown); + }, + methods: { + async init() { + await this.discoverNodes(); + }, + + async discoverNodes() { + this.loading = true; + this.error = null; + + try { + // Get nodes with stores already included + const nodesResponse = await fetch('/hosts'); + if (!nodesResponse.ok) { + throw new Error('Failed to fetch nodes'); + } + const nodes = (await nodesResponse.json()).data; + if (!nodes || nodes.length === 0) { + this.error = 'No nodes found. Make sure nodes are registered with the debug server.'; + return; + } + + // Transform nodes to include transaction state + const discoveredNodes = nodes.map(node => ({ + id: node.id, + stores: node.stores.map(store => ({ + storeId: store.storeId, + ranges: store.ranges, + transactions: [], + loading: false, + loaded: false, + error: null + })) + })); + + this.nodes = discoveredNodes.sort((a, b) => a.id - b.id); + + if (this.nodes.length === 0) { + this.error = 'No accessible nodes found.'; + } else { + // Don't load transactions immediately - they'll be loaded lazily when visible + this.setupIntersectionObserver(); + } + } catch (error) { + this.error = 'Failed to discover nodes: ' + error.message; + console.error('Node discovery error:', error); + } finally { + this.loading = false; + } + }, + + async loadStoreTransactions(nodeId, store) { + console.log(`Node ${nodeId} store ${store.storeId} loading lazily`); + store.loading = true; + store.error = null; + + try { + const response = await fetch(`/hosts/${nodeId}/stores/${store.storeId}/transactions`); + const data = (await response.json()).data; + console.log(data) + if (response.ok) { + store.transactions = data || []; + store.loaded = true; + } else { + store.error = data.error || 'Failed to load transactions'; + } + } catch (error) { + store.error = 'Error: ' + error.message; + console.error(`Error loading transactions for node ${nodeId}, store ${store.storeId}:`, error); + } finally { + store.loading = false; + } + }, + + setupIntersectionObserver() { + // Create intersection observer to lazy load transactions + this.intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const storeElement = entry.target; + const nodeId = storeElement.dataset.nodeId; + const storeId = parseInt(storeElement.dataset.storeId); + + // Find the store object and load transactions if not already loaded + const node = this.nodes.find(n => n.id === nodeId); + if (node) { + const store = node.stores.find(s => s.storeId === storeId); + if (store && !store.loaded && !store.loading) { + this.loadStoreTransactions(nodeId, store); + // Stop observing this element + this.intersectionObserver.unobserve(storeElement); + } + } + } + }); + }, { + root: null, // Use viewport as root + rootMargin: '50px', // Load 50px before entering viewport + threshold: 0.1 // Trigger when 10% visible + }); + + // Wait for DOM update then observe all store elements + this.$nextTick(() => { + this.observeStoreElements(); + }); + }, + + observeStoreElements() { + const storeElements = document.querySelectorAll('.command-store'); + storeElements.forEach(element => { + this.intersectionObserver.observe(element); + }); + }, + + getFilteredTransactions(transactions) { + if (!this.searchFilter) { + return transactions; + } + + const filter = this.searchFilter.toLowerCase(); + return transactions.filter(txn => { + // Search in transaction ID + if (txn.txnId && txn.txnId.toLowerCase().includes(filter)) { + return true; + } + + // Search in save status + if (txn.saveStatus && txn.saveStatus.toLowerCase().includes(filter)) { + return true; + } + + // Search in other fields if they exist + if (txn.durability && txn.durability.toLowerCase().includes(filter)) { + return true; + } + + if (txn.executeAt && txn.executeAt.toLowerCase().includes(filter)) { + return true; + } + + if (txn.participants && txn.participants.toLowerCase().includes(filter)) { + return true; + } + + return false; + }); + }, + + async refreshAll() { + await this.discoverNodes(); + }, + + async toggleRangeFilter(range) { + if (this.selectedRange === range) { + await this.clearRangeFilter(); + } else { + this.selectedRange = range; + await this.applyRangeFilter(range); + } + }, + + async clearRangeFilter() { + this.selectedRange = null; + // Reload all nodes without filter + await this.discoverNodes(); + }, + + async applyRangeFilter(range) { + this.loading = true; + this.error = null; + + try { + // Get nodes filtered by range on server side + const nodesResponse = await fetch(`/hosts?range=${encodeURIComponent(range)}`); + if (!nodesResponse.ok) { + throw new Error('Failed to fetch filtered nodes'); + } + const nodesWithStores = (await nodesResponse.json()).data; + + // Transform nodes to include transaction state + const discoveredNodes = nodesWithStores.map(nodeWithStores => ({ + id: nodeWithStores.id, + stores: nodeWithStores.stores.map(store => ({ + storeId: store.storeId, + ranges: store.ranges, + transactions: [], + loading: false, + loaded: false, + error: null + })) + })); + + this.nodes = discoveredNodes.sort((a, b) => a.id - b.id); + + if (this.nodes.length === 0) { + this.error = `No nodes found with range: ${range}`; + } else { + // Setup intersection observer for lazy loading + this.setupIntersectionObserver(); + } + } catch (error) { + this.error = 'Failed to filter by range: ' + error.message; + console.error('Range filter error:', error); + } finally { + this.loading = false; + } + }, + + getFilteredNodes() { + if (!this.searchFilter) + return this.nodes; + const res = []; + for (const node of this.nodes) { + const stores = this.getFilteredStores(node.stores); + if (stores.length > 0) { + const copy = {...node}; + copy.stores = stores; + res.push(copy); + } + } + return res; + }, + + getFilteredStores(stores) { + if (!this.searchFilter) + return stores; + + const res = []; + for (const store of stores) { + const copy = {...store}; + if (!store.loaded) + { + res.push(copy); + continue; + } + const transactions = this.getFilteredTransactions(store.transactions); + if (transactions.length > 0) { + + copy.transactions = transactions; + res.push(copy); + } + } + return res; + }, + + showTransactionDetails(transaction, nodeId = null, storeId = null, isFromDependency = false) { + this.selectedTransaction = transaction; + this.currentNodeId = nodeId; + this.currentStoreId = storeId; + + // Only add to breadcrumbs when navigating from dependencies + if (isFromDependency && transaction) { + this.transactionBreadcrumbs.push({ + txnId: transaction.txnId, + transaction: transaction, + nodeId: nodeId, + storeId: storeId + }); + } else if (!isFromDependency) { + // Reset breadcrumbs for new transaction chains (when clicked directly from store) + this.transactionBreadcrumbs = [{ + txnId: transaction.txnId, + transaction: transaction, + nodeId: nodeId, + storeId: storeId + }]; + } + + this.showTransactionPopup = true; + }, + + async showTransactionById(txnId) { + if (!this.currentNodeId || this.currentStoreId === null) { + alert('Cannot navigate to transaction: no current node/store context available.'); + return; + } + + try { + const response = await fetch(`/hosts/${this.currentNodeId}/stores/${this.currentStoreId}/transactions/${encodeURIComponent(txnId)}`); + const data = (await response.json()).data; + + if (response.ok) { + this.showTransactionDetails(data, this.currentNodeId, this.currentStoreId, true); + } else { + alert(`Transaction ${txnId} not found in node ${this.currentNodeId}, store ${this.currentStoreId}: ${data.error || 'Unknown error'}`); + } + } catch (error) { + alert(`Error fetching transaction ${txnId}: ${error.message}`); + console.error('Error fetching transaction:', error); + } + }, + + navigateToBreadcrumb(breadcrumbIndex) { + // Show the selected transaction + const breadcrumb = this.transactionBreadcrumbs[breadcrumbIndex]; + this.selectedTransaction = breadcrumb.transaction; + this.currentNodeId = breadcrumb.nodeId; + this.currentStoreId = breadcrumb.storeId; + + // Truncate breadcrumbs to only include items up to the selected one + this.transactionBreadcrumbs = this.transactionBreadcrumbs.slice(0, breadcrumbIndex + 1); + }, + + closeTransactionPopup() { + this.showTransactionPopup = false; + this.selectedTransaction = null; + this.currentNodeId = null; + this.currentStoreId = null; + this.transactionBreadcrumbs = []; + }, + + async showCommandsForKey(routingKey) { + if (!this.currentNodeId) { + alert('Cannot load CommandsForKey: no current node context available.'); + return; + } + + // Open the commands_for_key.html page in a new tab with the routing key and host + const url = `/commands_for_key.html?host=${encodeURIComponent(this.currentNodeId)}&key=${encodeURIComponent(routingKey)}`; + window.open(url, '_blank'); + }, + + closeCommandsForKeyPopup() { + this.showCommandsForKeyPopup = false; + this.commandsForKeyData = null; + }, + + handleKeyDown(event) { + if (event.key === 'Escape') { + if (this.showCommandsForKeyPopup) { + this.closeCommandsForKeyPopup(); + } else if (this.showTransactionPopup) { + this.closeTransactionPopup(); + } + } + } + }, + + beforeUnmount() { + // Clean up intersection observer + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + } + // Remove keyboard event listener + document.removeEventListener('keydown', this.handleKeyDown); + } +}).mount('#app'); \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/command_store.html b/accord-debug/src/main/resources/web/command_store.html new file mode 100644 index 0000000000..c1e855a458 --- /dev/null +++ b/accord-debug/src/main/resources/web/command_store.html @@ -0,0 +1,536 @@ + + + + + + + Cluster Debug Interface - Command Store + + + + + + + + +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ {{ error }} +
+ +
+ Loading transactions... +
+ +
+ +
+
+
+
No transactions found for this store
+
No transactions match your search criteria
+
+ +
+
+
+ +
{{ txn.saveStatus }}
+
+ +
+
+ Route: + + + + +
+
+ Execute At: + {{ txn.executeAt }} +
+
+ Durability: + {{ txn.durability }} +
+
+
+
+
+ +
+
+
Redundant Before
+
+
+
[{{ rangeData.tokenStart }}, {{ rangeData.tokenEnd }})
+
{{ rangeData.startEpoch || 'N/A' }} -> {{ rangeData.endEpoch || 'N/A' }}
+
+
+
+
{{ property.property }}:
+
{{ property.timestamp }}
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/command_store.js b/accord-debug/src/main/resources/web/command_store.js new file mode 100644 index 0000000000..544c423a52 --- /dev/null +++ b/accord-debug/src/main/resources/web/command_store.js @@ -0,0 +1,254 @@ +/* + * 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. + */ + +const { createApp } = Vue; + +createApp({ + components: { + 'transaction-widget': window.TransactionWidgetComponent, + 'navigation-widget': window.NavigationWidgetComponent, + 'host-selector': window.HostSelectorComponent, + 'routing-key-widget': window.RoutingKeyWidgetComponent + }, + data() { + return { + hosts: [], + selectedHostId: new URLSearchParams(window.location.search).get('host') || '', + selectedStoreId: new URLSearchParams(window.location.search).get('store') || '', + selectedProperty: null, + transactions: [], + propertyFilteredTxnIds: [], + redundantBeforeData: [], + loading: false, + error: null, + searchFilter: '' + }; + }, + computed: { + selectedHost() { + return this.hosts.find(host => host.id === this.selectedHostId) || null; + }, + + selectedStore() { + if (!this.selectedHost || !this.selectedStoreId) return null; + return this.selectedHost.stores.find(store => store.storeId.toString() === this.selectedStoreId.toString()) || null; + }, + + filteredTransactions() { + let filtered = this.transactions; + + // Apply search filter + if (this.searchFilter) { + const filter = this.searchFilter.toLowerCase(); + filtered = filtered.filter(txn => { + return Object.values(txn).some(value => + value && value.toString().toLowerCase().includes(filter) + ); + }); + } + + return filtered; + } + }, + mounted() { + this.loadHosts(); + }, + methods: { + async loadHosts() { + this.loading = true; + this.error = null; + + try { + const response = await fetch('/hosts'); + if (!response.ok) { + throw new Error(`Failed to load hosts: ${response.statusText}`); + } + + const result = await response.json(); + console.log('Hosts response:', result); + + this.hosts = result.data || []; + + // If host was in URL, handle initial selection + if (this.selectedHostId) { + await this.handleHostChange(); + } + } catch (error) { + console.error('Error loading hosts:', error); + this.error = error.message; + this.hosts = []; + } finally { + this.loading = false; + } + }, + + async handleHostChange() { + this.selectedStoreId = ''; + this.transactions = []; + this.selectedProperty = null; + this.propertyFilteredTxnIds = []; + + if (!this.selectedHostId) return; + + // If store ID was in URL, try to select it + const urlStoreId = new URLSearchParams(window.location.search).get('store'); + if (urlStoreId && this.selectedHost) { + const store = this.selectedHost.stores.find(s => s.storeId.toString() === urlStoreId); + if (store) { + this.selectedStoreId = urlStoreId; + await this.handleStoreChange(); + } + } + }, + + async handleStoreChange() { + this.transactions = []; + this.selectedProperty = null; + this.propertyFilteredTxnIds = []; + this.redundantBeforeData = []; + + if (!this.selectedStoreId) return; + + await Promise.all([ + this.loadTransactions(), + this.loadRedundantBefore() + ]); + + // Update URL with current selection + const url = new URL(window.location); + url.searchParams.set('host', this.selectedHostId); + url.searchParams.set('store', this.selectedStoreId); + window.history.replaceState({}, '', url); + }, + + async loadTransactions() { + if (!this.selectedHostId || !this.selectedStoreId) return; + + this.loading = true; + this.error = null; + + try { + const response = await fetch(`/hosts/${this.selectedHostId}/stores/${this.selectedStoreId}/transactions`); + if (!response.ok) { + throw new Error(`Failed to load transactions: ${response.statusText}`); + } + + const result = await response.json(); + console.log('Transactions response:', result); + + this.transactions = result.data || []; + } catch (error) { + console.error('Error loading transactions:', error); + this.error = error.message; + this.transactions = []; + } finally { + this.loading = false; + } + }, + + async loadRedundantBefore() { + if (!this.selectedHostId) return; + + try { + const response = await fetch(`/hosts/${this.selectedHostId}/redundant_before`); + if (!response.ok) { + throw new Error(`Failed to load redundant before data: ${response.statusText}`); + } + + const result = await response.json(); + console.log('Redundant before response:', result); + + // Transform the flat response into range-grouped format + const data = result.data || {}; + + data.map((v, idx) => { + return v.properties = [ + { property: 'GC_BEFORE', timestamp: v.gcBefore || '[0,0,0(KR),0]' }, + { property: 'SHARD_APPLIED', timestamp: v.shardApplied || '[0,0,0(KR),0]' }, + { property: 'QUORUM_APPLIED', timestamp: v.quorumApplied || '[0,0,0(KR),0]' }, + { property: 'LOCALLY_APPLIED', timestamp: v.locallyApplied || '[0,0,0(KR),0]' }, + { property: 'LOCALLY_DURABLE_TO_COMMAND_STORE', timestamp: v.locallyDurableToCommandStore || '[0,0,0(KR),0]' }, + { property: 'LOCALLY_DURABLE_TO_DATA_STORE', timestamp: v.locallyDurableToDataStore || '[0,0,0(KR),0]' }, + { property: 'LOCALLY_REDUNDANT', timestamp: v.locallyRedundant || '[1,0,0(KR),0]' }, + { property: 'LOCALLY_SYNCED', timestamp: v.locallySynced || '[0,0,0(KR),0]' }, + { property: 'LOCALLY_WITNESSED', timestamp: v.locallyWitnessed || '[0,0,0(KR),0]' }, + { property: 'PRE_BOOTSTRAP', timestamp: v.preBootstrap || '[1,0,0(KR),0]' } + ] + }); + + this.redundantBeforeData = data; + + } catch (error) { + console.error('Error loading redundant before data:', error); + // Don't set error state for redundant before, as it's supplementary data + this.redundantBeforeData = []; + } + }, + + async filterByProperty(property) { + if (this.selectedProperty === property) { + // Toggle off if same property clicked + this.clearPropertyFilter(); + return; + } + + this.selectedProperty = property; + this.loading = true; + this.error = null; + + try { + const response = await fetch(`/hosts/${this.selectedHostId}/stores/${this.selectedStoreId}/transactions?property=${encodeURIComponent(property)}`); + if (!response.ok) { + throw new Error(`Failed to load filtered transactions: ${response.statusText}`); + } + + const result = await response.json(); + console.log('Property filtered transactions response:', result); + + // Store the TxnIds that match the property filter + this.propertyFilteredTxnIds = (result.data || []).map(txn => txn.txnId); + } catch (error) { + console.error('Error loading property filtered transactions:', error); + this.error = error.message; + this.propertyFilteredTxnIds = []; + } finally { + this.loading = false; + } + }, + + clearPropertyFilter() { + this.selectedProperty = null; + this.propertyFilteredTxnIds = []; + }, + + isFilteredOut(txn) { + // If no property filter is active, nothing is filtered out + if (!this.selectedProperty) return false; + + // Transaction is filtered out if it's NOT in the property filtered list + return !this.propertyFilteredTxnIds.includes(txn.txnId); + }, + + openTransactionDetail(txnId) { + if (!txnId || txnId === 'null') return; + + const url = `/txn.html?host=${encodeURIComponent(this.selectedHostId)}&txn_id=${encodeURIComponent(txnId)}`; + window.open(url, '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes'); + } + } +}).mount('#app'); \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/command_store_tmp.html b/accord-debug/src/main/resources/web/command_store_tmp.html new file mode 100644 index 0000000000..3a5bddd8ac --- /dev/null +++ b/accord-debug/src/main/resources/web/command_store_tmp.html @@ -0,0 +1,434 @@ + + + + + + + Cluster Debug Interface - Command Store + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading command store data... +
+ +
+ No command store data found for {{ selectedHost }} +
+ +
+
+
+ Store ID: {{ entry.commandStoreId }} +
+ +
+
Safe to Read Map:
+
+ (empty map) +
+
+
+
+ +
+
{{ value }}
+
+
+
+ +
+
Ranges for Epoch:
+
+ (empty map) +
+
+ + + + + + + + + + + + + +
EpochRanges
{{ epoch }} +
+
+ {{ range }} +
+
+
+ {{ ranges }} +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/commands_for_key.html b/accord-debug/src/main/resources/web/commands_for_key.html new file mode 100644 index 0000000000..480d87b79f --- /dev/null +++ b/accord-debug/src/main/resources/web/commands_for_key.html @@ -0,0 +1,339 @@ + + + + + + + Cluster Debug Interface - Commands For Key + + + + + + +
+
+ {{ error }} +
+ +
+ Loading commands for key data... +
+ + + +
+ No commands found for key "{{ searchKey }}" on {{ selectedHost }} +
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/coordinations.html b/accord-debug/src/main/resources/web/coordinations.html new file mode 100644 index 0000000000..0c99cafd21 --- /dev/null +++ b/accord-debug/src/main/resources/web/coordinations.html @@ -0,0 +1,375 @@ + + + + + + + Cluster Debug Interface - Coordinations + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading coordination data... +
+ +
+ No coordination data found for {{ selectedHost }} +
+ +
+
+
+
+
{{ coordination.txnId }}
+
ID: {{ coordination.coordinationId }}
+
+
{{ coordination.kind }}
+
+
+
+ Description: + {{ coordination.description }} +
+
+ Nodes: + {{ coordination.nodes || 'N/A' }} +
+
+ Nodes In-flight: + {{ coordination.nodesInflight }} +
+
+ Nodes Contacted: + {{ coordination.nodesContacted }} +
+
+ Participants: + {{ coordination.participants || 'N/A' }} +
+
+ Replies: + {{ coordination.replies }} +
+
+ Tracker: + {{ coordination.tracker || 'N/A' }} +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/durability_service.html b/accord-debug/src/main/resources/web/durability_service.html new file mode 100644 index 0000000000..0a227b181a --- /dev/null +++ b/accord-debug/src/main/resources/web/durability_service.html @@ -0,0 +1,573 @@ + + + + + + + Cluster Debug Interface - Durability Service + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading durability service data... +
+ +
+ No durability service data found for {{ selectedHost }} +
+ +
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+

+ + {{ entry.stopping ? 'STOPPING' : 'RUNNING' }} + + + {{ entry.stopped ? 'STOPPED' : 'ACTIVE' }} + +

+
+ +
+
+ Last Started: + {{ formatTimestamp(entry.lastStartedAt) }} +
+
+ Cycle Started: + {{ formatTimestamp(entry.cycleStartedAt) }} +
+
+ Retries: + {{ entry.retries }} +
+
+ Min: + {{ entry.min || 'N/A' }} +
+
+ Requested By: + {{ entry.requestedBy || 'N/A' }} +
+
+ Active: + {{ entry.active || 'N/A' }} +
+
+ Waiting: +
{{ entry.waiting || 'N/A' }}
+
+
+ Node Offset: + {{ entry.nodeOffset }} +
+
+ Cycle Offset: + {{ entry.cycleOffset }} +
+
+ Active Index: + {{ entry.activeIndex }} +
+
+ Next Index: + {{ entry.nextIndex }} +
+
+ Next To Index: + {{ entry.nextToIndex }} +
+
+ End Index: + {{ entry.endIndex }} +
+
+ Current Splits: + {{ entry.currentSplits }} +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/durable_before.html b/accord-debug/src/main/resources/web/durable_before.html new file mode 100644 index 0000000000..b3d60393f2 --- /dev/null +++ b/accord-debug/src/main/resources/web/durable_before.html @@ -0,0 +1,352 @@ + + + + + + + Cluster Debug Interface - Durable Before + + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading durable before data... +
+ +
+ No durable before data found for {{ selectedHost }} +
+ +
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+
+ +
+
+ Quorum: + + + N/A +
+
+ Universal: + + + N/A +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/lamport_diagram.html b/accord-debug/src/main/resources/web/lamport_diagram.html new file mode 100644 index 0000000000..bc7bd8487b --- /dev/null +++ b/accord-debug/src/main/resources/web/lamport_diagram.html @@ -0,0 +1,724 @@ + + + + + + + Lamport Diagram - Accord Debug + + + + + +
+
+
+ +
+
+
+ {{ isDragOver ? 'Drop JSON file here' : 'Drop JSON file here' }} +
+
+ + +
+ +
+
+
+ +
+ {{ error }} +
+ +
+
+ {{ idx + 1 }} +
+
+ +
+
+
+
+ Message Types +
+ +
+
Show All
+
Hide All
+
+ +
+
+ {{ messageType.name }} +
+
+ +
+ +
+
+ Tags +
+ +
+
Show All Tags
+
Hide All Tags
+
+ +
+
+ {{ tag.name }} +
+
+ + +
+
+ +
+ + + + + + {{ process }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ truncateText(message.short_description) }} + + + +
+ +
+
+
Message Details
+ +
+

Basic Information

+

{{ selectedMessage.from }} -> {{ selectedMessage.to }}

+

{{selectedMessage.id}}#{{ selectedMessage.message_kind }}

+

{{ selectedMessage.short_description }}

+
+ +
+

Sent At: {{ formatDateTime(selectedMessage.sent_at) }}

+

Received At: {{ formatDateTime(selectedMessage.received_at) }}

+
+ +
+

Tags

+
+ {{ tag }} +
+
+ +
+

Description

+
{{ selectedMessage.long_description }}
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/lamport_diagram.js b/accord-debug/src/main/resources/web/lamport_diagram.js new file mode 100644 index 0000000000..edd6305aa5 --- /dev/null +++ b/accord-debug/src/main/resources/web/lamport_diagram.js @@ -0,0 +1,564 @@ +/* + * 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. + */ + +const { createApp } = Vue; + +createApp({ + data() { + return { + corellateReqRsp: true, + messageInput: '', + messages: [], + filteredMessages: [], + processedMessages: [], + processes: [], + selectedMessage: null, + highlightedMessages: [], + error: null, + + isDragOver: false, + isDragActive: false, + + // Filter state + messageTypes: [], // Array of {name, count, enabled} + messageTypeFilters: new Map(), // Map of message_kind -> boolean + tags: [], // Array of {name, count, enabled} + tagFilters: new Map(), // Map of tag -> boolean + + // Paging + activePage: 0, + pages: [], + messagesPerPage: 100, + + // Layout constants + diagramWidth: 1200, + diagramHeight: 800, + headerHeight: 50, + footerHeight: 30, + processSpacing: 500, + timeStep: 40, + selfLoopWidth: 100 + }; + }, + + mounted() { + // Load sample data on start + this.loadSampleData(); + + // Add keyboard event listener + document.addEventListener('keydown', this.handleKeyDown); + }, + + beforeUnmount() { + document.removeEventListener('keydown', this.handleKeyDown); + }, + + methods: { + formatDateTime(timestamp) { + if (!timestamp) return 'N/A'; + + try { + const date = new Date(timestamp); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = String(date.getFullYear()).slice(-2); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const milliseconds = String(date.getMilliseconds()).padStart(3, '0'); + + return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}:${milliseconds}`; + } catch (error) { + return timestamp; // Return original if parsing fails + } + }, + + truncateText(text, maxLength = 20) { + if (!text) return ''; + return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; + }, + + loadSampleData() { + const sampleData = []; + + this.messageInput = JSON.stringify(sampleData, null, 2); + this.loadMessages(); + }, + + loadMessages() { + this.error = null; + this.selectedMessage = null; + this.highlightedMessages = []; + + try { + this.messages = JSON.parse(this.messageInput); + + if (!Array.isArray(this.messages)) { + throw new Error('Input must be an array of messages'); + } + + this.processMessages(); + } catch (e) { + this.error = `Error parsing messages: ${e.message}`; + this.messages = []; + this.filteredMessages = []; + this.processedMessages = []; + this.processes = []; + } + }, + + // TODO: avoid looping over the messages multiple times + processMessages() { + // Extract message types and tags and initialize filters + this.extractMessageTypes(); + this.extractTags(); + + const seenSent = new Set(); + const seenReceived = new Set(); + // Filter messages based on current filter state + this.filteredMessages = this.messages.filter(msg => { + // Check message type filter + if (this.messageTypeFilters.get(msg.message_kind) === false) { + return false; + } + + // Check tag filters - message must have at least one enabled tag, or no tags required if all tags enabled + if (this.tags.length > 0) { + if (msg.tags && msg.tags.length > 0) { + const hasEnabledTag = msg.tags.some(tag => this.tagFilters.get(tag) !== false); + if (!hasEnabledTag) { + return false; + } + } + else + // If tag filtering is enabled, filter out messages with no tags + return false; + } + + seenSent.add(msg.sent_at); + seenReceived.add(msg.received_at); + return true; + }); + + const pagesTotal = Math.ceil(this.filteredMessages.length / this.messagesPerPage); + + this.pages = []; + for (let i = 0; i < pagesTotal; i++) + this.pages.push(i); + + // Extract all processes from filtered messages + const processSet = new Set(); + this.filteredMessages.forEach(msg => { + processSet.add(msg.from); + processSet.add(msg.to); + }); + this.processes = Array.from(processSet).sort(); + + let filteredMessages = this.filteredMessages.slice(this.activePage * this.messagesPerPage, this.activePage * this.messagesPerPage + this.messagesPerPage); + + // Create timestamp mapping for normalization from filtered messages + const allTimestamps = new Set(); + const reqReceivedAt = new Map(); + filteredMessages.forEach(msg => { + // if (this.corellateReqRsp && msg && msg.message_kind.endsWith("_REQ")) + // reqReceivedAt.set([msg.to, msg.id], msg.received_at); + allTimestamps.add(msg.sent_at); + allTimestamps.add(msg.received_at); + }); + + const sortedTimestamps = Array.from(new Set(allTimestamps)).sort((a, b) => a - b); + const timeMap = new Map(); + sortedTimestamps.forEach((timestamp, index) => { + timeMap.set(timestamp, index); + }); + + // Process filtered messages with normalized timestamps + this.processedMessages = filteredMessages.map(msg => { + var sent_at = msg.sent_at; + var received_at = (this.corellateReqRsp && msg.received_at) ? msg.received_at + 1 : msg.sent_at; + // if (this.corellateReqRsp && msg && msg.message_kind.endsWith("_RSP") && reqReceivedAt.has([msg.from, msg.id])) + // { + // sent_at = reqReceivedAt.get([msg.from, msg.id]); + // received_at = sent_at + 1; + // } + return { + ...msg, + normalizedSentAt: timeMap.get(sent_at), + normalizedReceivedAt: timeMap.get(received_at) + 1 + }; + }) + + // Calculate diagram dimensions + this.calculateDimensions(timeMap); + }, + + // TODO: message type counts are unused; do we want to use them? + extractMessageTypes() { + // Count all message types from all messages (not filtered) + const typeCount = new Map(); + this.messages.forEach(msg => { + const kind = msg.message_kind; + typeCount.set(kind, (typeCount.get(kind) || 0) + 1); + }); + + // Create or update message types array + const existingFilters = new Map(this.messageTypeFilters); + this.messageTypes = Array.from(typeCount.entries()) + .map(([name, count]) => ({ + name, + count, + enabled: existingFilters.has(name) ? existingFilters.get(name) : true + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + // Update filter state + this.messageTypeFilters.clear(); + this.messageTypes.forEach(type => { + this.messageTypeFilters.set(type.name, type.enabled); + }); + }, + + // TODO: tag counts are unused; do we want to use them? + extractTags() { + // Count all tags from all messages (not filtered) + const tagCount = new Map(); + this.messages.forEach(msg => { + if (msg.tags && Array.isArray(msg.tags)) { + msg.tags.forEach(tag => { + tagCount.set(tag, (tagCount.get(tag) || 0) + 1); + }); + } + }); + + // Create or update tags array + const existingTagFilters = new Map(this.tagFilters); + this.tags = Array.from(tagCount.entries()) + .map(([name, count]) => ({ + name, + count, + enabled: existingTagFilters.has(name) ? existingTagFilters.get(name) : true + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + // Update tag filter state + this.tagFilters.clear(); + this.tags.forEach(tag => { + this.tagFilters.set(tag.name, tag.enabled); + }); + }, + + /** + * Rendering + */ + + calculateDimensions(timeMap) { + const maxProcesses = this.processes.length; + const maxTime = timeMap.size > 0 ? Math.max(...Array.from(timeMap.values())) : 0; + + // Ensure minimum width for readability and add padding for process labels + this.diagramWidth = Math.max(1200, maxProcesses * this.processSpacing + 400); + + // Ensure adequate height for all time steps with extra padding + this.diagramHeight = Math.max(600, maxTime * this.timeStep + this.headerHeight + this.footerHeight + 200); + }, + + getProcessX(processName) { + const index = this.processes.indexOf(processName); + return 100 + index * this.processSpacing; + }, + + getMessageY(normalizedTime) { + return this.headerHeight + 50 + normalizedTime * this.timeStep; + }, + + getLabelX(message) { + if (message.from === message.to) { + return this.getProcessX(message.from) + this.selfLoopWidth / 2; + } + const fromX = this.getProcessX(message.from); + const toX = this.getProcessX(message.to); + return (fromX + toX) / 2; + }, + + getLabelY(message) { + if (message.from === message.to) { + return this.getMessageY(message.normalizedSentAt) - 15; + } + const fromY = this.getMessageY(message.normalizedSentAt); + const toY = this.getMessageY(message.normalizedReceivedAt); + return (fromY + toY) / 2 - 10; + }, + + getLabelWidth(text) { + // Approximate text width calculation + return text.length * 6; + }, + + getArrowPoints(message) { + const fromX = this.getProcessX(message.from); + const fromY = this.getMessageY(message.normalizedSentAt); + const toX = this.getProcessX(message.to); + const toY = this.getMessageY(message.normalizedReceivedAt); + + // Calculate arrow direction + const dx = toX - fromX; + const dy = toY - fromY; + const length = Math.sqrt(dx * dx + dy * dy); + + if (length === 0) return ''; + + const unitX = dx / length; + const unitY = dy / length; + + // Arrow head size + const arrowSize = 8; + + // Arrow head points + const tipX = toX - 4 * unitX; // Offset from circle + const tipY = toY - 4 * unitY; + + const leftX = tipX - arrowSize * unitX - 4 * unitY; + const leftY = tipY - arrowSize * unitY + 4 * unitX; + + const rightX = tipX - arrowSize * unitX + 4 * unitY; + const rightY = tipY - arrowSize * unitY - 4 * unitX; + + return `${tipX},${tipY} ${leftX},${leftY} ${rightX},${rightY}`; + }, + + getSelfMessagePath(message) { + const x = this.getProcessX(message.from); + const startY = this.getMessageY(message.normalizedSentAt); + const endY = startY + this.timeStep; // Land at next tick + const width = this.selfLoopWidth; + + // Create an arc that goes from current tick to next tick + // Arc goes out to the right and curves back to the process line + return `M ${x + 4} ${startY} + C ${x + width} ${startY} ${x + width} ${endY} ${x + 4} ${endY}`; + }, + + getSelfMessageArrowPoints(message) { + const x = this.getProcessX(message.from) - 3; + const startY = this.getMessageY(message.normalizedSentAt); + const endY = startY + this.timeStep; // Arrow moved 5px down from end point + + // Arrow pointing left (rotated 90 degrees from downward) + const arrowX = x + 4; + const arrowY = endY; + return `${arrowX},${arrowY} ${arrowX + 8},${arrowY - 4} ${arrowX + 8},${arrowY + 4}`; + }, + + onMessageClick(message) { + if (message === this.selectedMessage) + { + this.selectedMessage = null; + this.highlightedMessages = []; + return; + } + this.selectedMessage = message; + this.highlightRelatedMessages(message); + }, + + // TODO (required): implement highlights by checking if there's a match in tags + highlightRelatedMessages(message) { + if (!message.tags || message.tags.length === 0) { + this.highlightedMessages = [message]; + return; + } + + this.highlightedMessages = this.processedMessages.filter(msg => { + if (msg === message) return true; + if (!msg.tags || msg.tags.length === 0) return false; + + // Check if any tags match + return msg.tags.some(tag => message.tags.includes(tag)); + }); + }, + + isMessageHighlighted(message) { + return this.highlightedMessages.includes(message); + }, + + isProcessHighlighted(processName) { + return this.highlightedMessages.some(msg => + msg.from === processName || msg.to === processName + ); + }, + + handleKeyDown(event) { + if (event.key === 'Escape') { + this.selectedMessage = null; + this.highlightedMessages = []; + } + }, + + /** + * File upload handling + **/ + + // Drag and drop event handlers + handleDragEnter(event) { + event.preventDefault(); + this.isDragActive = true; + }, + + handleDragOver(event) { + event.preventDefault(); + this.isDragOver = true; + }, + + handleDragLeave(event) { + event.preventDefault(); + // Only reset drag state if leaving the drag zone completely + if (!event.currentTarget.contains(event.relatedTarget)) { + this.isDragOver = false; + this.isDragActive = false; + } + }, + + handleDrop(event) { + event.preventDefault(); + this.isDragOver = false; + this.isDragActive = false; + + const files = event.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + + // Check if it's a JSON file + if (file.type === 'application/json' || file.name.endsWith('.json')) { + this.readJsonFile(file); + } else { + this.error = `Invalid file type: ${file.type || 'unknown'}. Please upload a JSON file.`; + } + } + }, + + readJsonFile(file) { + this.error = null; + + const reader = new FileReader(); + + reader.onload = (event) => { + try { + const content = event.target.result; + this.messageInput = content; + + // Automatically load the messages from the file + this.messages = JSON.parse(content); + + if (!Array.isArray(this.messages)) { + throw new Error('File must contain an array of messages'); + } + + this.processMessages(); + + } catch (e) { + this.error = `Error reading file: ${e.message}`; + this.messages = []; + this.processedMessages = []; + this.processes = []; + } + }; + + reader.onerror = () => { + this.error = 'Error reading file. Please try again.'; + }; + + reader.readAsText(file); + }, + + + /** + * Message types + */ + + toggleMessageType(messageKind) { + const currentState = this.messageTypeFilters.get(messageKind); + this.messageTypeFilters.set(messageKind, !currentState); + + // Update messageTypes array + const typeIndex = this.messageTypes.findIndex(t => t.name === messageKind); + if (typeIndex >= 0) { + this.messageTypes[typeIndex].enabled = !currentState; + } + + // Reprocess messages with new filter + this.processMessages(); + }, + + enableAllMessageTypes() { + this.messageTypes.forEach(type => { + type.enabled = true; + this.messageTypeFilters.set(type.name, true); + }); + this.processMessages(); + }, + + disableAllMessageTypes() { + this.messageTypes.forEach(type => { + type.enabled = false; + this.messageTypeFilters.set(type.name, false); + }); + this.processMessages(); + }, + + /** + * Tags + */ + + toggleTag(tagName) { + const currentState = this.tagFilters.get(tagName); + this.tagFilters.set(tagName, !currentState); + + // Update tags array + const tagIndex = this.tags.findIndex(t => t.name === tagName); + if (tagIndex >= 0) { + this.tags[tagIndex].enabled = !currentState; + } + + // Reprocess messages with new filter + this.processMessages(); + }, + + enableAllTags() { + this.tags.forEach(tag => { + tag.enabled = true; + this.tagFilters.set(tag.name, true); + }); + this.processMessages(); + }, + + disableAllTags() { + this.tags.forEach(tag => { + tag.enabled = false; + this.tagFilters.set(tag.name, false); + }); + this.processMessages(); + }, + + /** + * Paging + */ + togglePage(page) { + this.activePage = page; + this.processMessages(); + } + } +}).mount('#app'); \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/progress_log.html b/accord-debug/src/main/resources/web/progress_log.html new file mode 100644 index 0000000000..b56c28124c --- /dev/null +++ b/accord-debug/src/main/resources/web/progress_log.html @@ -0,0 +1,567 @@ + + + + + + + Cluster Debug Interface - Progress Log + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading progress log data... +
+ +
+ No progress log data found for {{ selectedHost }} +
+ +
+
+ Command Store {{ commandStoreId }} +
+ +
+
+
+ {{ keyspaceTable }} +
+ +
+
+
+
+
{{ entry.txnId }}
+
{{ entry.tableId }}
+
+
+ + {{ entry.contactEveryone ? 'Contact Everyone' : 'Selective Contact' }} + +
+
+ +
+ +
+
⏳ Waiting State
+
+ Initialised: + + {{ entry.waitingIsUninitialised ? 'No' : 'Yes' }} + +
+
+ Blocked Until: + {{ entry.waitingBlockedUntil }} +
+
+ Home Satisfies: + {{ entry.waitingHomeSatisfies }} +
+
+ Progress: + {{ entry.waitingProgress }} +
+
+ Retry Count: + {{ entry.waitingRetryCounter }} +
+
+ Key Tracker: + {{ entry.waitingPackedKeyTrackerBits }} +
+
+ Scheduled At: + {{ formatTimestampFromMillis(entry.waitingScheduledAt) }} +
+
+ + +
+
🏠 Home State
+
+ Phase: + {{ entry.homePhase }} +
+
+ Progress: + {{ entry.homeProgress }} +
+
+ Retry Count: + {{ entry.homeRetryCounter }} +
+
+ Scheduled At: + {{ formatTimestampFromMillis(entry.homeScheduledAt) }} +
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/redundant_before.html b/accord-debug/src/main/resources/web/redundant_before.html new file mode 100644 index 0000000000..190a6bf469 --- /dev/null +++ b/accord-debug/src/main/resources/web/redundant_before.html @@ -0,0 +1,551 @@ + + + + + + + Cluster Debug Interface - Redundant Before + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading redundant before data... +
+ +
+ No redundant before data found for {{ selectedHost }} +
+ +
+
+ Command Store {{ commandStoreId }} +
+ +
+
+
+ {{ keyspaceTable }} +
+ +
+
+
+

{{ entry.tokenStart }} → {{ entry.tokenEnd }}

+

Epochs: {{ entry.startEpoch }} → {{ entry.endEpoch }}

+
+ +
+
+ Table ID: + {{ entry.tableId }} +
+
+ GC Before: + {{ entry.gcBefore || 'N/A' }} +
+
+ Shard Applied: + {{ entry.shardApplied || 'N/A' }} +
+
+ Quorum Applied: + {{ entry.quorumApplied || 'N/A' }} +
+
+ Locally Applied: + {{ entry.locallyApplied || 'N/A' }} +
+
+ Locally Durable (Command Store): + {{ entry.locallyDurableToCommandStore || 'N/A' }} +
+
+ Locally Durable (Data Store): + {{ entry.locallyDurableToDataStore || 'N/A' }} +
+
+ Locally Redundant: + {{ entry.locallyRedundant || 'N/A' }} +
+
+ Locally Synced: + {{ entry.locallySynced || 'N/A' }} +
+
+ Locally Witnessed: + {{ entry.locallyWitnessed || 'N/A' }} +
+
+ Pre Bootstrap: + {{ entry.preBootstrap || 'N/A' }} +
+
+ Stale Until At Least: + {{ entry.staleUntilAtLeast || 'N/A' }} +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/styles.css b/accord-debug/src/main/resources/web/styles.css new file mode 100644 index 0000000000..9330481fb4 --- /dev/null +++ b/accord-debug/src/main/resources/web/styles.css @@ -0,0 +1,1188 @@ +/* + * 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. + */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f5f5f5; + color: #333; +} + +/* Navigation Bar */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + background: white; + border-bottom: 2px solid #e0e0e0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.nav-links { + display: flex; + gap: 20px; +} + +.nav-link { + padding: 8px 16px; + text-decoration: none; + color: #6c757d; + border-radius: 4px; + transition: all 0.3s; + font-weight: 500; +} + +.nav-link:hover { + background: #f8f9fa; + color: #495057; +} + +.nav-link.active { + background: #3498db; + color: white; +} + +.refresh-button .btn-refresh { + padding: 8px 16px; + background: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.refresh-button .btn-refresh:hover { + background: #218838; +} + +.refresh-button .btn-refresh:disabled { + background: #bdc3c7; + cursor: not-allowed; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + background: white; + border-bottom: 2px solid #e0e0e0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.header h1 { + color: #2c3e50; + margin: 0; +} + +.controls { + display: flex; + gap: 10px; +} + +.btn { + padding: 10px 16px; + background: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.btn:hover { + background: #2980b9; +} + +.btn:disabled { + background: #bdc3c7; + cursor: not-allowed; +} + +.health-status { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: #fff3cd; + border-left: 4px solid #ffc107; + margin: 0; +} + +.health-status.healthy { + background: #d4edda; + border-left-color: #28a745; +} + +.node-count { + font-weight: bold; + color: #6c757d; +} + +.error { + background: #f8d7da; + color: #721c24; + padding: 15px 20px; + border-left: 4px solid #dc3545; +} + +/* Search Container */ +.search-container { + padding: 15px 20px; + background: white; + border-bottom: 1px solid #e0e0e0; +} + +.search-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e0e0e0; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.3s, box-shadow 0.3s; +} + +.search-input:focus { + outline: none; + border-color: #3498db; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); +} + +.search-input::placeholder { + color: #6c757d; + font-style: italic; +} + +.search-info { + margin-top: 8px; + font-size: 12px; + color: #6c757d; + background: #f8f9fa; + padding: 6px 12px; + border-radius: 4px; + border-left: 3px solid #3498db; +} + +.loading { + text-align: center; + padding: 40px; + color: #6c757d; + font-style: italic; +} + +/* Nodes Container - Vertical Layout */ +.nodes-container { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* Node Strip - Vertical arrangement */ +.node-strip { + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + overflow: hidden; +} + +.node-header { + background: #34495e; + color: white; + padding: 5px 5px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.node-header h2 { + margin: 0; + font-size: 12px; +} + +.store-count { + background: rgba(255,255,255,0.2); + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; +} + +/* Stores Container - Horizontal Layout with scroll */ +.stores-container { + display: flex; + overflow-x: auto; + padding: 5px; + gap: 5px; + background: #f8f9fa; +} + +/* Command Store - Vertical within horizontal layout */ +.command-store { + flex: 0 0 300px; /* Fixed width, no grow/shrink */ + background: white; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + overflow: hidden; + min-height: 200px; +} + +.store-header { + background: #3498db; + color: white; + padding: 5px 5px; + text-align: center; +} + +.store-header h3 { + margin: 0; + font-size: 12px; + font-weight: 600; +} + +/* Transactions List */ +.transactions-list { + padding: 5px; + max-height: 400px; + overflow-y: auto; +} + +.transaction-item { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 3px 3px; + margin-bottom: 3px; + font-size: 12px; +} + +.txn-id { + font-family: 'Courier New', monospace; + font-weight: bold; + color: #495057; + margin-bottom: 4px; + word-break: break-all; + padding: 2px 6px; + display: inline; +} + +.txn-status { + font-size: 11px; + color: #6c757d; + background: #e9ecef; + padding: 2px 6px; + border-radius: 3px; + display: inline; +} + +/* Status-specific colors */ +.txn-status.Applied { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.txn-status.Committed { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.txn-status.Accepted { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.txn-status.PreAccepted { + background: #e2e3e5; + color: #383d41; + border: 1px solid #d6d8db; +} + +.txn-status.NotDefined { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.txn-status.Invalidated { + background: #343a40; + color: #ffffff; + border: 1px solid #495057; +} + +.txn-status.Truncated { + background: #e7e8ea; + color: #495057; + border: 1px solid #ced4da; +} + +.txn-status.ReadyToExecute { + background: #cce5ff; + color: #004085; + border: 1px solid #99d6ff; +} + +.txn-status.Executing { + background: #ffe066; + color: #664d00; + border: 1px solid #ffcc00; +} + +.txn-status.WaitingToApply { + background: #f0e6ff; + color: #6f42c1; + border: 1px solid #d9b3ff; +} + +/* Durability-based colors (fallback) */ +.txn-status.Durable { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.txn-status.NotDurable { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; +} + +.txn-status.Local { + background: #e2e3e5; + color: #383d41; + border: 1px solid #d6d8db; +} + +/* Unknown status */ +.txn-status.Unknown { + background: #f8f9fa; + color: #6c757d; + border: 1px solid #dee2e6; +} + +.loading-txns, .no-txns { + text-align: center; + padding: 20px; + color: #6c757d; + font-style: italic; + font-size: 12px; +} + +.error-txns { + text-align: center; + padding: 20px; + color: #dc3545; + font-size: 12px; +} + +/* Scrollbar styling */ +.stores-container::-webkit-scrollbar { + height: 8px; +} + +.stores-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.stores-container::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +.stores-container::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +.transactions-list::-webkit-scrollbar { + width: 6px; +} + +.transactions-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.transactions-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .nodes-container { + padding: 10px; + } + + .command-store { + flex: 0 0 250px; + } +} + +/* Redundant Before Visualization */ +.redundant-before-store { + min-height: 250px; /* Slightly taller than command stores */ +} + +.store-ranges { + font-size: 10px; + color: #e3f2fd; + margin-top: 2px; + font-style: italic; +} + +.redundant-before-content { + padding: 8px; + overflow-y: auto; +} + +.loading-rb, .no-rb { + text-align: center; + padding: 30px; + color: #6c757d; + font-style: italic; + font-size: 12px; +} + +.error-rb { + text-align: center; + padding: 20px; + color: #dc3545; + font-size: 12px; +} + +.redundant-before-entry { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 12px; + margin-bottom: 12px; + font-size: 11px; +} + +.range-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + padding-bottom: 6px; + border-bottom: 1px solid #e9ecef; +} + +.range-name { + font-family: 'Courier New', monospace; + font-weight: bold; + color: #2c3e50; + font-size: 12px; +} + +.epoch-info { + font-size: 10px; + color: #6c757d; + background: #e9ecef; + padding: 2px 6px; + border-radius: 3px; +} + +.stale-info { + margin-bottom: 8px; + padding: 6px; + background: #fff3cd; + border-left: 3px solid #ffc107; + border-radius: 4px; +} + +.stale-label { + font-weight: bold; + color: #856404; + margin-right: 6px; +} + +.stale-value { + font-family: 'Courier New', monospace; + color: #856404; + font-size: 10px; +} + +.properties-container { + margin-bottom: 10px; +} + +.property-entry { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + margin-bottom: 3px; + background: #ffffff; + border: 1px solid #e9ecef; + border-radius: 4px; + transition: background-color 0.2s; +} + +.property-entry:hover { + background: #f1f3f5; +} + +.property-name { + font-weight: 600; + color: #495057; + font-size: 10px; + flex-grow: 1; +} + +.property-bound { + font-family: 'Courier New', monospace; + color: #28a745; + font-size: 10px; + background: #d4edda; + padding: 2px 6px; + border-radius: 3px; + border: 1px solid #c3e6cb; +} + +.bounds-info { + border-top: 1px solid #e9ecef; + padding-top: 8px; + margin-top: 8px; +} + +.bound-entry { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.bound-label { + font-weight: bold; + color: #6c757d; + font-size: 10px; +} + +.bound-value { + font-family: 'Courier New', monospace; + color: #495057; + font-size: 10px; + background: #e9ecef; + padding: 2px 6px; + border-radius: 3px; +} + +/* Special styling for different bound types */ +.bound-entry:has(.bound-label:contains("Bootstrapped")) .bound-value { + background: #d1ecf1; + color: #0c5460; +} + +.bound-entry:has(.bound-label:contains("GC")) .bound-value { + background: #f8d7da; + color: #721c24; +} + +/* Scrollbar for redundant before content */ +.redundant-before-content::-webkit-scrollbar { + width: 6px; +} + +.redundant-before-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.redundant-before-content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.redundant-before-content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Main Menu Styles */ +.main-menu { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.main-menu .navbar h1 { + color: white; + margin: 0; + font-size: 24px; +} + +.menu-container { + display: flex; + justify-content: center; + align-items: center; + min-height: calc(100vh - 80px); + padding: 40px 20px; +} + +.menu-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 30px; + max-width: 800px; + width: 100%; +} + +.menu-card { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + text-align: center; + transition: transform 0.3s, box-shadow 0.3s; +} + +.menu-card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 40px rgba(0,0,0,0.3); +} + +.menu-card h2 { + color: #2c3e50; + margin-bottom: 15px; + font-size: 22px; +} + +.menu-card p { + color: #6c757d; + margin-bottom: 25px; + line-height: 1.6; + font-size: 14px; +} + +.menu-button { + display: inline-block; + background: linear-gradient(45deg, #3498db, #2980b9); + color: white; + padding: 12px 30px; + border-radius: 25px; + text-decoration: none; + font-weight: 600; + transition: all 0.3s; + font-size: 14px; +} + +.menu-button:hover { + transform: scale(1.05); + box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4); +} + +/* Range Filtering Styles */ +.store-ranges { + margin-top: 8px; + background-color: #f8f9fa; + border-radius: 4px; +} + +.ranges-label { + font-size: 12px; + font-weight: bold; + color: #6c757d; + margin-right: 8px; +} + +.range-item { + display: inline-block; + margin: 2px 2px; + padding: 2px 2px; + background-color: #e9ecef; + border: 1px solid #dee2e6; + color: #262626; + border-radius: 1px; + font-size: 9px; + font-family: monospace; + cursor: pointer; + transition: all 0.2s ease; +} + +.range-item:hover { + background-color: #d1ecf1; + border-color: #bee5eb; + transform: translateY(-1px); +} + +.range-item.active { + background-color: #0dcaf0; + color: white; + border-color: #0dcaf0; +} + +.range-filter-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 20px; + margin: 10px 20px; + background-color: #d1ecf1; + border: 1px solid #bee5eb; + border-radius: 6px; + font-size: 14px; +} + +.filter-text { + color: #0c5460; +} + +.clear-filter-btn { + padding: 4px 12px; + background-color: #0dcaf0; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background-color 0.2s ease; +} + +.clear-filter-btn:hover { + background-color: #31d2f2; +} + +/* Transaction Popup Styles */ +.clickable { + cursor: pointer; + transition: background-color 0.2s ease; +} + +.transaction-item.clickable:hover { + background-color: #e3f2fd; + border-radius: 4px; +} + +.popup-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.popup-content { + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + max-width: 1000px; + max-height: 90vh; + width: 90%; + overflow-y: auto; +} + +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #e0e0e0; + background-color: #f8f9fa; + border-radius: 8px 8px 0 0; +} + +.popup-header h2 { + margin: 0; + color: #2c3e50; + font-size: 1.5em; +} + +.close-btn { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #6c757d; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.close-btn:hover { + background-color: #e9ecef; + color: #495057; +} + +.popup-body { + padding: 20px; +} + +.txn-detail-section { + margin-bottom: 24px; + border-bottom: 1px solid #f0f0f0; + padding-bottom: 16px; +} + +.txn-detail-section:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.txn-detail-section h3 { + color: #2c3e50; + margin-bottom: 12px; + font-size: 1.1em; + font-weight: 600; +} + +.txn-detail-item { + display: flex; + margin-bottom: 8px; + align-items: flex-start; +} + +.txn-detail-label { + font-weight: 600; + color: #495057; + min-width: 140px; + margin-right: 12px; +} + +.txn-detail-value { + color: #212529; + word-break: break-all; + flex: 1; +} + +.txn-detail-value.participants { + font-family: 'Courier New', monospace; + background-color: #f8f9fa; + padding: 8px; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +.txn-detail-value.status { + padding: 2px 8px; + border-radius: 4px; + font-weight: 600; + font-size: 0.85em; +} + +.txn-detail-list { + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + padding: 12px; +} + +.txn-detail-list-item { + font-family: 'Courier New', monospace; + padding: 4px 0; + border-bottom: 1px solid #e9ecef; + word-break: break-all; +} + +.txn-detail-list-item:last-child { + border-bottom: none; +} + +.txn-detail-list-item.clickable { + cursor: pointer; + transition: background-color 0.2s ease; +} + +.txn-detail-list-item.clickable:hover { + background-color: #e3f2fd; + border-radius: 4px; +} + +.txn-bubble-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.txn-bubble { + display: inline-block; + padding: 4px 8px; + background-color: #e3f2fd; + border: 1px solid #bbdefb; + border-radius: 16px; + font-family: 'Courier New', monospace; + font-size: 0.85em; + color: #1565c0; + transition: all 0.2s ease; + word-break: break-all; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.txn-bubble.clickable { + cursor: pointer; +} + +.txn-bubble.clickable:hover { + background-color: #2196f3; + color: white; + border-color: #1976d2; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +/* Breadcrumb Styles */ +.breadcrumb-container { + padding: 16px 20px; + background-color: #f8f9fa; + border-bottom: 1px solid #e0e0e0; + font-size: 0.9em; +} + +.breadcrumb-label { + font-weight: 600; + color: #495057; + margin-bottom: 8px; + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.breadcrumb-nav { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.breadcrumb-item { + display: flex; + align-items: center; + gap: 8px; +} + +.breadcrumb-link { + color: #0066cc; + font-family: 'Courier New', monospace; + padding: 4px 8px; + border-radius: 4px; + background-color: white; + border: 1px solid #d1ecf1; + transition: all 0.2s ease; + cursor: pointer; +} + +.breadcrumb-link:hover { + background-color: #0066cc; + color: white; + border-color: #0066cc; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.breadcrumb-current { + color: #2c3e50; + font-family: 'Courier New', monospace; + padding: 4px 8px; + border-radius: 4px; + background-color: #e3f2fd; + border: 1px solid #bbdefb; + font-weight: 600; +} + +.breadcrumb-arrow { + color: #6c757d; + font-weight: bold; + font-size: 1.1em; +} + +@media (max-width: 768px) { + .menu-grid { + grid-template-columns: 1fr; + } + + .menu-card { + padding: 20px; + } + + .range-filter-info { + flex-direction: column; + gap: 8px; + text-align: center; + } + + .range-item { + font-size: 10px; + padding: 3px 6px; + } + + .popup-content { + width: 95%; + max-height: 95vh; + } + + .popup-header { + padding: 15px; + } + + .popup-body { + padding: 15px; + } + + .txn-detail-item { + flex-direction: column; + gap: 4px; + } + + .txn-detail-label { + min-width: auto; + margin-right: 0; + font-size: 0.9em; + } + + .txn-bubble { + max-width: 150px; + font-size: 0.75em; + padding: 3px 6px; + } + + .txn-bubble-container { + gap: 6px; + } + + .breadcrumb-container { + padding: 12px 15px; + font-size: 0.8em; + } + + .breadcrumb-nav { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .breadcrumb-link, + .breadcrumb-current { + font-size: 0.75em; + padding: 3px 6px; + } + + .breadcrumb-arrow { + display: none; + } + + .breadcrumb-item { + flex-direction: column; + gap: 4px; + align-items: flex-start; + } + + .breadcrumb-item::after { + content: "↓"; + color: #6c757d; + font-weight: bold; + align-self: center; + } + + .breadcrumb-item:last-child::after { + display: none; + } +} + +/* CommandsForKey specific styles */ +.routing-key { + color: #3498db; + text-decoration: underline; + cursor: pointer; + transition: color 0.2s; +} + +.routing-key:hover { + color: #2980b9; +} + +.commands-for-key-table { + display: table; + width: 100%; + border-collapse: collapse; + margin-top: 15px; +} + +.cfk-table-header { + display: table-row; + background-color: #f8f9fa; + font-weight: bold; +} + +.cfk-table-row { + display: table-row; + border-bottom: 1px solid #e9ecef; +} + +.cfk-table-row:nth-child(even) { + background-color: #f8f9fa; +} + +.cfk-table-row:hover { + background-color: #e9ecef; +} + +.cfk-header-cell, +.cfk-cell { + display: table-cell; + padding: 8px 12px; + text-align: left; + vertical-align: top; + border-right: 1px solid #dee2e6; + word-wrap: break-word; + max-width: 120px; + font-size: 0.85em; +} + +.cfk-header-cell { + background-color: #e9ecef; + font-weight: 600; + color: #495057; +} + +.cfk-header-cell:last-child, +.cfk-cell:last-child { + border-right: none; +} + +.cfk-cell.txn-id { + min-width: 180px; +} \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/topologies.html b/accord-debug/src/main/resources/web/topologies.html new file mode 100644 index 0000000000..78efe7e0bf --- /dev/null +++ b/accord-debug/src/main/resources/web/topologies.html @@ -0,0 +1,464 @@ + + + + + + + Cluster Debug Interface - Topologies + + + + + + + +
+ + + +
+ {{ error }} +
+ +
+ +
+ +
+ Loading topology data... +
+ +
+ No topology data found for {{ selectedHost }} +
+ +
+
+
+ Epoch {{ topology.epoch ? topology.epoch.epoch : 'Unknown' }} + + Metadata:{{ topology.epoch.readyMetadata || 'N/A' }} | + Coordinate:{{ topology.epoch.readyCoordinate || 'N/A' }} | + Data:{{ topology.epoch.readyData || 'N/A' }} | + Reads:{{ topology.epoch.readyReads || 'N/A' }} + +
+
+ + {{ topology.epoch.ready ? 'READY' : 'NOT READY' }} + +
+
+ +
+
+ No table changes in this epoch +
+ +
+
+
+ {{ tableEpoch.keyspaceName }}.{{ tableEpoch.tableName }} +
+ +
+
+ Added: +
+ {{ node }} +
+
+ +
+ Removed: +
+ {{ node }} +
+
+ +
+ Synced/Closed: +
+ {{ node }} + {{ node }} +
+
+ +
+ Retired: +
+ {{ node }} +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/txn.html b/accord-debug/src/main/resources/web/txn.html new file mode 100644 index 0000000000..958a14f546 --- /dev/null +++ b/accord-debug/src/main/resources/web/txn.html @@ -0,0 +1,615 @@ + + + + + + + Transaction Search - Multi-Host Query + + + + +
+
+ Searching across all hosts... +
+ +
+ {{ error }} +
+ +
+
+
{{ totalHosts }}
+
Total Hosts
+
+
+
{{ hostsWithData }}
+
Hosts with Data
+
+
+
{{ totalTransactions }}
+
Total Transactions
+
+
+
{{ hostsWithErrors }}
+
Hosts with Errors
+
+
+ +
+
+
+ {{ hostResult.hostname }} + + {{ hostResult.statusText }} + +
+ +
+
+ Querying host... +
+ +
+ {{ hostResult.errorMessage }} +
+ +
+
+
+ Store {{ transaction.commandStoreId }} + + {{ transaction.saveStatus || 'Unknown' }} + +
+ +
+
+ Route: + {{ transaction.route }} +
+
+ Durability: + {{ transaction.durability }} +
+
+ Execute At: + {{ transaction.executeAt }} +
+
+ Executes At Least: + {{ transaction.executesAtLeast }} +
+
+ Txn: + {{ transaction.txn }} +
+
+ Deps: + {{ transaction.deps }} +
+
+ Waiting On: + {{ transaction.waitingOn }} +
+
+ Writes: + {{ transaction.writes }} +
+
+ Result: + {{ transaction.result }} +
+
+ + +
+
+ 🚫 Transaction Blocked By ({{ transaction.blockedBy.length }} dependencies) +
+
+
+ {{ blocking.blockedBy }} + Depth {{ blocking.depth }} +
+
+
+ Reason: + {{ blocking.reason }} +
+
+ Keyspace: + {{ blocking.keyspaceName }} +
+
+ Table: + {{ blocking.tableName }} +
+
+ Key: + {{ blocking.key }} +
+
+ Blocking Status: + {{ blocking.saveStatus }} +
+
+ Blocking Execute At: + {{ blocking.executeAt }} +
+
+
+
+
+
+ +
+ No transaction data found +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/accord-debug/src/main/resources/web/vue.global.js b/accord-debug/src/main/resources/web/vue.global.js new file mode 100644 index 0000000000..a63d243f00 --- /dev/null +++ b/accord-debug/src/main/resources/web/vue.global.js @@ -0,0 +1,18227 @@ +/** +* vue v3.5.18 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +var Vue = (function (exports) { + 'use strict'; + + /*! #__NO_SIDE_EFFECTS__ */ + // @__NO_SIDE_EFFECTS__ + function makeMap(str) { + const map = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map[key] = 1; + return (val) => val in map; + } + + const EMPTY_OBJ = Object.freeze({}) ; + const EMPTY_ARR = Object.freeze([]) ; + const NOOP = () => { + }; + const NO = () => false; + const isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter + (key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); + const isModelListener = (key) => key.startsWith("onUpdate:"); + const extend = Object.assign; + const remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } + }; + const hasOwnProperty$1 = Object.prototype.hasOwnProperty; + const hasOwn = (val, key) => hasOwnProperty$1.call(val, key); + const isArray = Array.isArray; + const isMap = (val) => toTypeString(val) === "[object Map]"; + const isSet = (val) => toTypeString(val) === "[object Set]"; + const isDate = (val) => toTypeString(val) === "[object Date]"; + const isRegExp = (val) => toTypeString(val) === "[object RegExp]"; + const isFunction = (val) => typeof val === "function"; + const isString = (val) => typeof val === "string"; + const isSymbol = (val) => typeof val === "symbol"; + const isObject = (val) => val !== null && typeof val === "object"; + const isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); + }; + const objectToString = Object.prototype.toString; + const toTypeString = (value) => objectToString.call(value); + const toRawType = (value) => { + return toTypeString(value).slice(8, -1); + }; + const isPlainObject = (val) => toTypeString(val) === "[object Object]"; + const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; + const isReservedProp = /* @__PURE__ */ makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" + ); + const isBuiltInDirective = /* @__PURE__ */ makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" + ); + const cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return (str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; + }; + const camelizeRE = /-(\w)/g; + const camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : ""); + } + ); + const hyphenateRE = /\B([A-Z])/g; + const hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() + ); + const capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); + }); + const toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } + ); + const hasChanged = (value, oldValue) => !Object.is(value, oldValue); + const invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } + }; + const def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); + }; + const looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; + }; + const toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; + }; + let _globalThis; + const getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); + }; + function genCacheKey(source, options) { + return source + JSON.stringify( + options, + (_, val) => typeof val === "function" ? val.toString() : val + ); + } + + const PatchFlagNames = { + [1]: `TEXT`, + [2]: `CLASS`, + [4]: `STYLE`, + [8]: `PROPS`, + [16]: `FULL_PROPS`, + [32]: `NEED_HYDRATION`, + [64]: `STABLE_FRAGMENT`, + [128]: `KEYED_FRAGMENT`, + [256]: `UNKEYED_FRAGMENT`, + [512]: `NEED_PATCH`, + [1024]: `DYNAMIC_SLOTS`, + [2048]: `DEV_ROOT_FRAGMENT`, + [-1]: `CACHED`, + [-2]: `BAIL` + }; + + const slotFlagsText = { + [1]: "STABLE", + [2]: "DYNAMIC", + [3]: "FORWARDED" + }; + + const GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol"; + const isGloballyAllowed = /* @__PURE__ */ makeMap(GLOBALS_ALLOWED); + + const range = 2; + function generateCodeFrame(source, start = 0, end = source.length) { + start = Math.max(0, Math.min(start, source.length)); + end = Math.max(0, Math.min(end, source.length)); + if (start > end) return ""; + let lines = source.split(/(\r?\n)/); + const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); + lines = lines.filter((_, idx) => idx % 2 === 0); + let count = 0; + const res = []; + for (let i = 0; i < lines.length; i++) { + count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0); + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) continue; + const line = j + 1; + res.push( + `${line}${" ".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}` + ); + const lineLength = lines[j].length; + const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0; + if (j === i) { + const pad = start - (count - (lineLength + newLineSeqLength)); + const length = Math.max( + 1, + end > count ? lineLength - pad : end - start + ); + res.push(` | ` + " ".repeat(pad) + "^".repeat(length)); + } else if (j > i) { + if (end > count) { + const length = Math.max(Math.min(end - count, lineLength), 1); + res.push(` | ` + "^".repeat(length)); + } + count += lineLength + newLineSeqLength; + } + } + break; + } + } + return res.join("\n"); + } + + function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } + } + const listDelimiterRE = /;(?![^(]*\))/g; + const propertyDelimiterRE = /:([^]+)/; + const styleCommentRE = /\/\*[^]*?\*\//g; + function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; + } + function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; + } + function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); + } + function normalizeProps(props) { + if (!props) return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; + } + + const HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; + const SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; + const MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; + const VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; + const isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS); + const isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS); + const isMathMLTag = /* @__PURE__ */ makeMap(MATH_TAGS); + const isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS); + + const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; + const isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs); + const isBooleanAttr = /* @__PURE__ */ makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` + ); + function includeBooleanAttr(value) { + return !!value || value === ""; + } + const isKnownHtmlAttr = /* @__PURE__ */ makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` + ); + const isKnownSvgAttr = /* @__PURE__ */ makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` + ); + function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; + } + + const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; + function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => `\\${s}` + ); + } + + function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; + } + function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); + } + function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); + } + + const isRef$1 = (val) => { + return !!(val && val["__v_isRef"] === true); + }; + const toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef$1(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); + }; + const replacer = (_key, val) => { + if (isRef$1(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; + }; + const stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); + }; + + function normalizeCssVarValue(value) { + if (value == null) { + return "initial"; + } + if (typeof value === "string") { + return value === "" ? " " : value; + } + if (typeof value !== "number" || !Number.isFinite(value)) { + { + console.warn( + "[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:", + value + ); + } + } + return String(value); + } + + function warn$2(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); + } + + let activeEffectScope; + class EffectScope { + constructor(detached = false) { + this.detached = detached; + /** + * @internal + */ + this._active = true; + /** + * @internal track `on` calls, allow `on` call multiple times + */ + this._on = 0; + /** + * @internal + */ + this.effects = []; + /** + * @internal + */ + this.cleanups = []; + this._isPaused = false; + this.parent = activeEffectScope; + if (!detached && activeEffectScope) { + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else { + warn$2(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + if (++this._on === 1) { + this.prevScope = activeEffectScope; + activeEffectScope = this; + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + if (this._on > 0 && --this._on === 0) { + activeEffectScope = this.prevScope; + this.prevScope = void 0; + } + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } + } + function effectScope(detached) { + return new EffectScope(detached); + } + function getCurrentScope() { + return activeEffectScope; + } + function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (!failSilently) { + warn$2( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } + } + + let activeSub; + const pausedQueueEffects = /* @__PURE__ */ new WeakSet(); + class ReactiveEffect { + constructor(fn) { + this.fn = fn; + /** + * @internal + */ + this.deps = void 0; + /** + * @internal + */ + this.depsTail = void 0; + /** + * @internal + */ + this.flags = 1 | 4; + /** + * @internal + */ + this.next = void 0; + /** + * @internal + */ + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope && activeEffectScope.active) { + activeEffectScope.effects.push(this); + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= -65; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + if (activeSub !== this) { + warn$2( + "Active effect was not restored correctly - this is likely a Vue internal bug." + ); + } + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= -3; + } + } + stop() { + if (this.flags & 1) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= -2; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } + } + let batchDepth = 0; + let batchedSub; + let batchedComputed; + function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; + } + function startBatch() { + batchDepth++; + } + function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; + } + function prepareDeps(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + link.version = -1; + link.prevActiveLink = link.dep.activeLink; + link.dep.activeLink = link; + } + } + function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link = tail; + while (link) { + const prev = link.prevDep; + if (link.version === -1) { + if (link === tail) tail = prev; + removeSub(link); + removeDep(link); + } else { + head = link; + } + link.dep.activeLink = link.prevActiveLink; + link.prevActiveLink = void 0; + link = prev; + } + sub.deps = head; + sub.depsTail = tail; + } + function isDirty(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; + } + function refreshComputed(computed) { + if (computed.flags & 4 && !(computed.flags & 16)) { + return; + } + computed.flags &= -17; + if (computed.globalVersion === globalVersion) { + return; + } + computed.globalVersion = globalVersion; + if (!computed.isSSR && computed.flags & 128 && (!computed.deps && !computed._dirty || !isDirty(computed))) { + return; + } + computed.flags |= 2; + const dep = computed.dep; + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed; + shouldTrack = true; + try { + prepareDeps(computed); + const value = computed.fn(computed._value); + if (dep.version === 0 || hasChanged(value, computed._value)) { + computed.flags |= 128; + computed._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed); + computed.flags &= -3; + } + } + function removeSub(link, soft = false) { + const { dep, prevSub, nextSub } = link; + if (prevSub) { + prevSub.nextSub = nextSub; + link.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link.nextSub = void 0; + } + if (dep.subsHead === link) { + dep.subsHead = nextSub; + } + if (dep.subs === link) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= -5; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } + } + function removeDep(link) { + const { prevDep, nextDep } = link; + if (prevDep) { + prevDep.nextDep = nextDep; + link.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link.nextDep = void 0; + } + } + function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const e = new ReactiveEffect(fn); + if (options) { + extend(e, options); + } + try { + e.run(); + } catch (err) { + e.stop(); + throw err; + } + const runner = e.run.bind(e); + runner.effect = e; + return runner; + } + function stop(runner) { + runner.effect.stop(); + } + let shouldTrack = true; + const trackStack = []; + function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; + } + function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; + } + function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } + } + + let globalVersion = 0; + class Link { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } + } + class Dep { + // TODO isolatedDeclarations "__v_skip" + constructor(computed) { + this.computed = computed; + this.version = 0; + /** + * Link between this dep and the current active effect + */ + this.activeLink = void 0; + /** + * Doubly linked list representing the subscribing effects (tail) + */ + this.subs = void 0; + /** + * For object property deps cleanup + */ + this.map = void 0; + this.key = void 0; + /** + * Subscriber counter + */ + this.sc = 0; + /** + * @internal + */ + this.__v_skip = true; + { + this.subsHead = void 0; + } + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link = this.activeLink; + if (link === void 0 || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + } + addSub(link); + } else if (link.version === -1) { + link.version = this.version; + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + link.prevDep = activeSub.depsTail; + link.nextDep = void 0; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + if (activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub + }, + debugInfo + ) + ); + } + return link; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (true) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & 8)) { + head.sub.onTrigger( + extend( + { + effect: head.sub + }, + debugInfo + ) + ); + } + } + } + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + ; + link.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } + } + function addSub(link) { + link.dep.sc++; + if (link.sub.flags & 4) { + const computed = link.dep.computed; + if (computed && !link.dep.subs) { + computed.flags |= 4 | 16; + for (let l = computed.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link.dep.subs; + if (currentTail !== link) { + link.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link; + } + if (link.dep.subsHead === void 0) { + link.dep.subsHead = link; + } + link.dep.subs = link; + } + } + const targetMap = /* @__PURE__ */ new WeakMap(); + const ITERATE_KEY = Symbol( + "Object iterate" + ); + const MAP_KEY_ITERATE_KEY = Symbol( + "Map keys iterate" + ); + const ARRAY_ITERATE_KEY = Symbol( + "Array iterate" + ); + function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + { + dep.track({ + target, + type, + key + }); + } + } + } + function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + { + dep.trigger({ + target, + type, + key, + newValue, + oldValue, + oldTarget + }); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); + } + function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); + } + + function reactiveReadArray(array) { + const raw = toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return isShallow(array) ? raw : raw.map(toReactive); + } + function shallowReadArray(arr) { + track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; + } + const arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, toReactive); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toReactive(value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply(this, "filter", fn, thisArg, (v) => v.map(toReactive), arguments); + }, + find(fn, thisArg) { + return apply(this, "find", fn, thisArg, toReactive, arguments); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply(this, "findLast", fn, thisArg, toReactive, arguments); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimisation required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", toReactive); + } + }; + function iterator(self, method, wrapValue) { + const arr = shallowReadArray(self); + const iter = arr[method](); + if (arr !== self && !isShallow(self)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (result.value) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; + } + const arrayProto = Array.prototype; + function apply(self, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self); + const needsWrap = arr !== self && !isShallow(self); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toReactive(item), index, self); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; + } + function reduce(self, method, fn, args) { + const arr = shallowReadArray(self); + let wrappedFn = fn; + if (arr !== self) { + if (!isShallow(self)) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, toReactive(item), index, self); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self); + }; + } + } + return arr[method](wrappedFn, ...args); + } + function searchProxy(self, method, args) { + const arr = toRaw(self); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && isProxy(args[0])) { + args[0] = toRaw(args[0]); + return arr[method](...args); + } + return res; + } + function noTracking(self, method, args = []) { + pauseTracking(); + startBatch(); + const res = toRaw(self)[method].apply(self, args); + endBatch(); + resetTracking(); + return res; + } + + const isNonTrackableKeys = /* @__PURE__ */ makeMap(`__proto__,__v_isRef,__isVue`); + const builtInSymbols = new Set( + /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) + ); + function hasOwnProperty(key) { + if (!isSymbol(key)) key = String(key); + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); + } + class BaseReactiveHandler { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + isRef(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (isRef(res)) { + return targetIsArray && isIntegerKey(key) ? res : res.value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } + } + class MutableReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + if (!this._isShallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArray(target) && isRef(oldValue) && !isRef(value)) { + if (isOldValueReadonly) { + return false; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + isRef(target) ? target : receiver + ); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } + } + class ReadonlyReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + { + warn$2( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + { + warn$2( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + } + const mutableHandlers = /* @__PURE__ */ new MutableReactiveHandler(); + const readonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(); + const shallowReactiveHandlers = /* @__PURE__ */ new MutableReactiveHandler(true); + const shallowReadonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(true); + + const toShallow = (value) => value; + const getProto = (v) => Reflect.getPrototypeOf(v); + function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + }, + // iterable protocol + [Symbol.iterator]() { + return this; + } + }; + }; + } + function createReadonlyMethod(type) { + return function(...args) { + { + const key = args[0] ? `on key "${args[0]}" ` : ``; + warn$2( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; + } + function createInstrumentations(readonly, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly && track(toRaw(target), "iterate", ITERATE_KEY); + return Reflect.get(target, "size", target); + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive; + !readonly && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const proto = getProto(target); + const hadKey = proto.has.call(target, value); + if (!hadKey) { + target.add(value); + trigger(target, "add", value, value); + } + return this; + }, + set(key, value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else { + checkIdentityKeys(target, has, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; + }, + delete(key) { + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else { + checkIdentityKeys(target, has, key); + } + const oldValue = get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + }, + clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = isMap(target) ? new Map(target) : new Set(target) ; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0, + oldTarget + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly, shallow); + }); + return instrumentations; + } + function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; + } + const mutableCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, false) + }; + const shallowCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, true) + }; + const readonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, false) + }; + const shallowReadonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, true) + }; + function checkIdentityKeys(target, has, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has.call(target, rawKey)) { + const type = toRawType(target); + warn$2( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } + } + + const reactiveMap = /* @__PURE__ */ new WeakMap(); + const shallowReactiveMap = /* @__PURE__ */ new WeakMap(); + const readonlyMap = /* @__PURE__ */ new WeakMap(); + const shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); + function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1 /* COMMON */; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2 /* COLLECTION */; + default: + return 0 /* INVALID */; + } + } + function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(toRawType(value)); + } + function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); + } + function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); + } + function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); + } + function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); + } + function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + { + warn$2( + `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String( + target + )}` + ); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const targetType = getTargetType(target); + if (targetType === 0 /* INVALID */) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const proxy = new Proxy( + target, + targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; + } + function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); + } + function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); + } + function isShallow(value) { + return !!(value && value["__v_isShallow"]); + } + function isProxy(value) { + return value ? !!value["__v_raw"] : false; + } + function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; + } + function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; + } + const toReactive = (value) => isObject(value) ? reactive(value) : value; + const toReadonly = (value) => isObject(value) ? readonly(value) : value; + + function isRef(r) { + return r ? r["__v_isRef"] === true : false; + } + function ref(value) { + return createRef(value, false); + } + function shallowRef(value) { + return createRef(value, true); + } + function createRef(rawValue, shallow) { + if (isRef(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); + } + class RefImpl { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + { + this.dep.track({ + target: this, + type: "get", + key: "value" + }); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + { + this.dep.trigger({ + target: this, + type: "set", + key: "value", + newValue, + oldValue + }); + } + } + } + } + function triggerRef(ref2) { + if (ref2.dep) { + { + ref2.dep.trigger({ + target: ref2, + type: "set", + key: "value", + newValue: ref2._value + }); + } + } + } + function unref(ref2) { + return isRef(ref2) ? ref2.value : ref2; + } + function toValue(source) { + return isFunction(source) ? source() : unref(source); + } + const shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } + }; + function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); + } + class CustomRefImpl { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } + } + function customRef(factory) { + return new CustomRefImpl(factory); + } + function toRefs(object) { + if (!isProxy(object)) { + warn$2(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; + } + class ObjectRefImpl { + constructor(_object, _key, _defaultValue) { + this._object = _object; + this._key = _key; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + } + get value() { + const val = this._object[this._key]; + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(toRaw(this._object), this._key); + } + } + class GetterRefImpl { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } + } + function toRef(source, key, defaultValue) { + if (isRef(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } + } + function propertyToRef(source, key, defaultValue) { + const val = source[key]; + return isRef(val) ? val : new ObjectRefImpl(source, key, defaultValue); + } + + class ComputedRefImpl { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + /** + * @internal + */ + this._value = void 0; + /** + * @internal + */ + this.dep = new Dep(this); + /** + * @internal + */ + this.__v_isRef = true; + // TODO isolatedDeclarations "__v_isReadonly" + // A computed is also a subscriber that tracks other deps + /** + * @internal + */ + this.deps = void 0; + /** + * @internal + */ + this.depsTail = void 0; + /** + * @internal + */ + this.flags = 16; + /** + * @internal + */ + this.globalVersion = globalVersion - 1; + /** + * @internal + */ + this.next = void 0; + // for backwards compat + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } + } + get value() { + const link = this.dep.track({ + target: this, + type: "get", + key: "value" + }) ; + refreshComputed(this); + if (link) { + link.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } else { + warn$2("Write operation failed: computed value is readonly"); + } + } + } + function computed$1(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + if (debugOptions && !isSSR) { + cRef.onTrack = debugOptions.onTrack; + cRef.onTrigger = debugOptions.onTrigger; + } + return cRef; + } + + const TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" + }; + const TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" + }; + + const INITIAL_WATCHER_VALUE = {}; + const cleanupMap = /* @__PURE__ */ new WeakMap(); + let activeWatcher = void 0; + function getCurrentWatcher() { + return activeWatcher; + } + function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } else if (!failSilently) { + warn$2( + `onWatcherCleanup() was called when there was no active watcher to associate with.` + ); + } + } + function watch$1(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const warnInvalidSource = (s) => { + (options.onWarn || warn$2)( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (isRef(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect.stop(); + if (scope && scope.active) { + remove(scope.effects, effect); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + watchHandle(); + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect.flags & 1) || !effect.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + oldValue = newValue; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect = new ReactiveEffect(getter); + effect.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect); + cleanup = effect.onStop = () => { + const cleanups = cleanupMap.get(effect); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect); + } + }; + { + effect.onTrack = options.onTrack; + effect.onTrigger = options.onTrigger; + } + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect.run(); + } + watchHandle.pause = effect.pause.bind(effect); + watchHandle.resume = effect.resume.bind(effect); + watchHandle.stop = watchHandle; + return watchHandle; + } + function traverse(value, depth = Infinity, seen) { + if (depth <= 0 || !isObject(value) || value["__v_skip"]) { + return value; + } + seen = seen || /* @__PURE__ */ new Set(); + if (seen.has(value)) { + return value; + } + seen.add(value); + depth--; + if (isRef(value)) { + traverse(value.value, depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen); + } + } + } + return value; + } + + const stack$1 = []; + function pushWarningContext(vnode) { + stack$1.push(vnode); + } + function popWarningContext() { + stack$1.pop(); + } + let isWarning = false; + function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack$1.length ? stack$1[stack$1.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; + } + function getComponentTrace() { + let currentVNode = stack$1[stack$1.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; + } + function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; + } + function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; + } + function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; + } + function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } + } + function assertNumber(val, type) { + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } + } + + const ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER", + "COMPONENT_UPDATE": 15, + "15": "COMPONENT_UPDATE", + "APP_UNMOUNT_CLEANUP": 16, + "16": "APP_UNMOUNT_CLEANUP" + }; + const ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush", + [15]: "component update", + [16]: "app unmount cleanup function" + }; + function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } + } + function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } else { + warn$1( + `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}` + ); + } + } + function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = ErrorTypeStrings$1[type] ; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); + } + function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } + } + + const queue = []; + let flushIndex = -1; + const pendingPostFlushCbs = []; + let activePostFlushCbs = null; + let postFlushIndex = 0; + const resolvedPromise = /* @__PURE__ */ Promise.resolve(); + let currentFlushPromise = null; + const RECURSION_LIMIT = 100; + function nextTick(fn) { + const p = currentFlushPromise || resolvedPromise; + return fn ? p.then(this ? fn.bind(this) : fn) : p; + } + function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; + } + function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } + } + function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } + } + function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); + } + function flushPreFlushCbs(instance, seen, i = flushIndex + 1) { + { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= -2; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= -2; + } + } + } + } + function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + { + seen = seen || /* @__PURE__ */ new Map(); + } + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + if (cb.flags & 4) { + cb.flags &= -2; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= -2; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } + } + const getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; + function flushJobs(seen) { + { + seen = seen || /* @__PURE__ */ new Map(); + } + const check = (job) => checkRecursiveUpdates(seen, job) ; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (check(job)) { + continue; + } + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= -2; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(seen); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } + } + function checkRecursiveUpdates(seen, fn) { + const count = seen.get(fn) || 0; + if (count > RECURSION_LIMIT) { + const instance = fn.i; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } + seen.set(fn, count + 1); + return false; + } + + let isHmrUpdating = false; + const hmrDirtyComponents = /* @__PURE__ */ new Map(); + { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; + } + const map = /* @__PURE__ */ new Map(); + function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); + } + function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); + } + function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; + } + function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; + } + function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + instance.update(); + isHmrUpdating = false; + }); + } + function reload(id, newComp) { + const record = map.get(id); + if (!record) return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + const oldComp = normalizeClassComponent(instance.type); + let dirtyInstances = hmrDirtyComponents.get(oldComp); + if (!dirtyInstances) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set()); + } + dirtyInstances.add(instance); + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + dirtyInstances.add(instance); + instance.ceReload(newComp.styles); + dirtyInstances.delete(instance); + } else if (instance.parent) { + queueJob(() => { + isHmrUpdating = true; + instance.parent.update(); + isHmrUpdating = false; + dirtyInstances.delete(instance); + }); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + if (instance.root.ce && instance !== instance.root) { + instance.root.ce._removeChildStyle(oldComp); + } + } + queuePostFlushCb(() => { + hmrDirtyComponents.clear(); + }); + } + function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } + } + function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; + } + + let devtools$1; + let buffer = []; + let devtoolsNotInstalled = false; + function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } + } + function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } + } + function devtoolsInitApp(app, version) { + emit$1("app:init" /* APP_INIT */, app, version, { + Fragment, + Text, + Comment, + Static + }); + } + function devtoolsUnmountApp(app) { + emit$1("app:unmount" /* APP_UNMOUNT */, app); + } + const devtoolsComponentAdded = /* @__PURE__ */ createDevtoolsComponentHook("component:added" /* COMPONENT_ADDED */); + const devtoolsComponentUpdated = /* @__PURE__ */ createDevtoolsComponentHook("component:updated" /* COMPONENT_UPDATED */); + const _devtoolsComponentRemoved = /* @__PURE__ */ createDevtoolsComponentHook( + "component:removed" /* COMPONENT_REMOVED */ + ); + const devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } + }; + /*! #__NO_SIDE_EFFECTS__ */ + // @__NO_SIDE_EFFECTS__ + function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; + } + const devtoolsPerfStart = /* @__PURE__ */ createDevtoolsPerformanceHook("perf:start" /* PERFORMANCE_START */); + const devtoolsPerfEnd = /* @__PURE__ */ createDevtoolsPerformanceHook("perf:end" /* PERFORMANCE_END */); + function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; + } + function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit" /* COMPONENT_EMIT */, + component.appContext.app, + component, + event, + params + ); + } + + let currentRenderingInstance = null; + let currentScopeId = null; + function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; + } + function pushScopeId(id) { + currentScopeId = id; + } + function popScopeId() { + currentScopeId = null; + } + const withScopeId = (_id) => withCtx; + function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; + } + + function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } + } + function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; + } + function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } + } + + const TeleportEndKey = Symbol("_vte"); + const isTeleport = (type) => type.__isTeleport; + const isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); + const isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); + const isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; + const isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; + const resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + warn$1( + `Current renderer does not support string target for Teleports. (missing querySelector renderer option)` + ); + return null; + } else { + const target = select(targetSelector); + if (!target && !isTeleportDisabled(props)) { + warn$1( + `Failed to locate Teleport target with selector "${targetSelector}". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.` + ); + } + return target; + } + } else { + if (!targetSelector && !isTeleportDisabled(props)) { + warn$1(`Invalid Teleport target: ${targetSelector}`); + } + return targetSelector; + } + }; + const TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { shapeFlag, children, dynamicChildren } = n2; + if (isHmrUpdating) { + optimized = false; + dynamicChildren = null; + } + if (n1 == null) { + const placeholder = n2.el = createComment("teleport start") ; + const mainAnchor = n2.anchor = createComment("teleport end") ; + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + const mount = (container2, anchor2) => { + if (shapeFlag & 16) { + if (parentComponent && parentComponent.isCE) { + parentComponent.ce._teleportTarget = container2; + } + mountChildren( + children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = () => { + const target = n2.target = resolveTarget(n2.props, querySelector); + const targetAnchor = prepareAnchor(target, n2, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (!disabled) { + mount(target, targetAnchor); + updateCssVars(n2, false); + } + } else if (!disabled) { + warn$1( + "Invalid Teleport target on mount:", + target, + `(${typeof target})` + ); + } + }; + if (disabled) { + mount(container, mainAnchor); + updateCssVars(n2, true); + } + if (isTeleportDeferred(n2.props)) { + n2.el.__isMounted = false; + queuePostRenderEffect(() => { + mountToTarget(); + delete n2.el.__isMounted; + }, parentSuspense); + } else { + mountToTarget(); + } + } else { + if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) { + queuePostRenderEffect(() => { + TeleportImpl.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + }, parentSuspense); + return; + } + n2.el = n1.el; + n2.targetStart = n1.targetStart; + const mainAnchor = n2.anchor = n1.anchor; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, false); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = n2.target = resolveTarget( + n2.props, + querySelector + ); + if (nextTarget) { + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } else { + warn$1( + "Invalid Teleport target on update:", + target, + `(${typeof target})` + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (shapeFlag & 16) { + const shouldRemove = doRemove || !isTeleportDisabled(props); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport + }; + function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!isReorder || isTeleportDisabled(props)) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } + } + function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } + }, hydrateChildren) { + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + if (target) { + const disabled = isTeleportDisabled(vnode.props); + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + vnode.anchor = hydrateChildren( + nextSibling(node), + vnode, + parentNode(node), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + vnode.targetStart = targetNode; + vnode.targetAnchor = targetNode && nextSibling(targetNode); + } else { + vnode.anchor = nextSibling(node); + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } + return vnode.anchor && nextSibling(vnode.anchor); + } + const Teleport = TeleportImpl; + function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } + } + function prepareAnchor(target, vnode, createText, insert) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target); + insert(targetAnchor, target); + } + return targetAnchor; + } + + const leaveCbKey = Symbol("_leaveCb"); + const enterCbKey$1 = Symbol("_enterCb"); + function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; + } + const TransitionHookValidator = [Function, Array]; + const BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator + }; + const recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; + }; + const BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + if (!children || !children.length) { + return; + } + const child = findNonCommentChild(children); + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(innerChild, oldInnerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } + }; + function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + } + } + } + return child; + } + const BaseTransition = BaseTransitionImpl; + function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; + } + function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook(hook, [el]); + }, + enter(el) { + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + const done = el[enterCbKey$1] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook(cancelHook, [el]); + } else { + callHook(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey$1] = void 0; + }; + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove) { + const key2 = String(vnode.key); + if (el[enterCbKey$1]) { + el[enterCbKey$1]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove(); + } + callHook(onBeforeLeave, [el]); + let called = false; + const done = el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove(); + if (cancelled) { + callHook(onLeaveCancelled, [el]); + } else { + callHook(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; + } + function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } + } + function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } + } + function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } + } + function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; + } + + /*! #__NO_SIDE_EFFECTS__ */ + // @__NO_SIDE_EFFECTS__ + function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; + } + + function useId() { + const i = getCurrentInstance(); + if (i) { + return (i.appContext.config.idPrefix || "v") + "-" + i.ids[0] + i.ids[1]++; + } else { + warn$1( + `useId() is called when there is no active component instance to be associated with.` + ); + } + return ""; + } + function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; + } + + const knownTemplateRefs = /* @__PURE__ */ new WeakSet(); + function useTemplateRef(key) { + const i = getCurrentInstance(); + const r = shallowRef(null); + if (i) { + const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs; + let desc; + if ((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable) { + warn$1(`useTemplateRef('${key}') already exists.`); + } else { + Object.defineProperty(refs, key, { + enumerable: true, + get: () => r.value, + set: (val) => r.value = val + }); + } + } else { + warn$1( + `useTemplateRef() is called when there is no active component instance to be associated with.` + ); + } + const ret = readonly(r) ; + { + knownTemplateRefs.add(ret); + } + return ret; + } + + function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref } = rawRef; + if (!owner) { + warn$1( + `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.` + ); + return; + } + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? () => false : (key) => { + { + if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) { + warn$1( + `Template ref "${key}" used on a non-ref value. It will not work in the production build.` + ); + } + if (knownTemplateRefs.has(rawSetupState[key])) { + return false; + } + } + return hasOwn(rawSetupState, key); + }; + if (oldRef != null && oldRef !== ref) { + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (isRef(oldRef)) { + oldRef.value = null; + } + } + if (isFunction(ref)) { + callWithErrorHandling(ref, owner, 12, [value, refs]); + } else { + const _isString = isString(ref); + const _isRef = isRef(ref); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref) ? setupState[ref] : refs[ref] : ref.value; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref] = [refValue]; + if (canSetSetupRef(ref)) { + setupState[ref] = refs[ref]; + } + } else { + ref.value = [refValue]; + if (rawRef.k) refs[rawRef.k] = ref.value; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref] = value; + if (canSetSetupRef(ref)) { + setupState[ref] = value; + } + } else if (_isRef) { + ref.value = value; + if (rawRef.k) refs[rawRef.k] = value; + } else { + warn$1("Invalid template ref type:", ref, `(${typeof ref})`); + } + }; + if (value) { + doSet.id = -1; + queuePostRenderEffect(doSet, parentSuspense); + } else { + doSet(); + } + } else { + warn$1("Invalid template ref type:", ref, `(${typeof ref})`); + } + } + } + + let hasLoggedMismatchError = false; + const logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; + }; + const isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; + const isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); + const getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; + }; + const isComment = (node) => node.nodeType === 8; + function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp, + createText, + nextSibling, + parentNode, + remove, + insert, + createComment + } + } = rendererInternals; + const hydrate = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + { + def(node, "__vnode", vnode, true); + def(node, "__vueParentComponent", parentComponent, true); + } + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref != null) { + setRef(ref, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode; + const forcePatch = type === "input" || type === "option"; + { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + const cls = content.getAttribute("class"); + if (cls) content.$cls = cls; + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + let hasWarned = false; + while (next) { + if (!isMismatchAllowed(el, 1 /* CHILDREN */)) { + if (!hasWarned) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + hasWarned = true; + } + logMismatchError(); + } + const cur = next; + next = next.nextSibling; + remove(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + if (el.textContent !== clientText) { + if (!isMismatchAllowed(el, 0 /* TEXT */)) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${el.textContent} + - expected on client: ${vnode.children}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if (// #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement) { + patchProp(el, key, null, props[key], void 0, parentComponent); + } + } + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasWarned = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!isMismatchAllowed(container, 1 /* CHILDREN */)) { + if (!hasWarned) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + hasWarned = true; + } + logMismatchError(); + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isMismatchAllowed(node.parentElement, 1 /* CHILDREN */)) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate, hydrateNode]; + } + function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + if (el.$cls) { + actual = el.$cls; + delete el.$cls; + } else { + actual = el.getAttribute("class"); + } + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2 /* CLASS */; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3 /* STYLE */; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4 /* ATTRIBUTE */; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; + } + function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); + } + function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; + } + function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; + } + function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; + } + function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]); + expectedMap.set(`--${getEscapedCssVarName(key)}`, value); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } + } + const allowMismatchAttr = "data-allow-mismatch"; + const MismatchTypeString = { + [0 /* TEXT */]: "text", + [1 /* CHILDREN */]: "children", + [2 /* CLASS */]: "class", + [3 /* STYLE */]: "style", + [4 /* ATTRIBUTE */]: "attribute" + }; + function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 /* TEXT */ || allowedType === 1 /* CHILDREN */) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + const allowedAttr = el && el.getAttribute(allowMismatchAttr); + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 /* TEXT */ && list.includes("children")) { + return true; + } + return list.includes(MismatchTypeString[allowedType]); + } + } + + const requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); + const cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); + const hydrateOnIdle = (timeout = 1e4) => (hydrate) => { + const id = requestIdleCallback(hydrate, { timeout }); + return () => cancelIdleCallback(id); + }; + function elementIsVisibleInViewport(el) { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth); + } + const hydrateOnVisible = (opts) => (hydrate, forEach) => { + const ob = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!e.isIntersecting) continue; + ob.disconnect(); + hydrate(); + break; + } + }, opts); + forEach((el) => { + if (!(el instanceof Element)) return; + if (elementIsVisibleInViewport(el)) { + hydrate(); + ob.disconnect(); + return false; + } + ob.observe(el); + }); + return () => ob.disconnect(); + }; + const hydrateOnMediaQuery = (query) => (hydrate) => { + if (query) { + const mql = matchMedia(query); + if (mql.matches) { + hydrate(); + } else { + mql.addEventListener("change", hydrate, { once: true }); + return () => mql.removeEventListener("change", hydrate); + } + } + }; + const hydrateOnInteraction = (interactions = []) => (hydrate, forEach) => { + if (isString(interactions)) interactions = [interactions]; + let hasHydrated = false; + const doHydrate = (e) => { + if (!hasHydrated) { + hasHydrated = true; + teardown(); + hydrate(); + e.target.dispatchEvent(new e.constructor(e.type, e)); + } + }; + const teardown = () => { + forEach((el) => { + for (const i of interactions) { + el.removeEventListener(i, doHydrate); + } + }); + }; + forEach((el) => { + for (const i of interactions) { + el.addEventListener(i, doHydrate, { once: true }); + } + }); + return teardown; + }; + function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } + } + + const isAsyncWrapper = (i) => !!i.type.__asyncLoader; + /*! #__NO_SIDE_EFFECTS__ */ + // @__NO_SIDE_EFFECTS__ + function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate) { + let patched = false; + (instance.bu || (instance.bu = [])).push(() => patched = true); + const performHydrate = () => { + if (patched) { + { + warn$1( + `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': it was updated before lazy hydration performed.` + ); + } + return; + } + hydrate(); + }; + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + performHydrate, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : performHydrate; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || false) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createVNode(loadingComponent); + } + }; + } + }); + } + function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; + } + + const isKeepAlive = (vnode) => vnode.type.__isKeepAlive; + const KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + invalidateMount(instance2.m); + invalidateMount(instance2.a); + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + { + devtoolsComponentAdded(instance2); + } + { + instance2.__keepAliveStorageContainer = storageContainer; + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName(vnode.type); + if (name && !filter(name)) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (cached && (!current || !isSameVNodeType(cached, current))) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + if (isSuspense(instance.subTree.type)) { + queuePostRenderEffect(() => { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + }, instance.subTree.suspense); + } else { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return current = null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + if (vnode.type === Comment) { + current = null; + return vnode; + } + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + vnode.shapeFlag &= -257; + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } + }; + const KeepAlive = KeepAliveImpl; + function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p) => matches(p, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + pattern.lastIndex = 0; + return pattern.test(name); + } + return false; + } + function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); + } + function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); + } + function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } + } + function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); + } + function resetShapeFlag(vnode) { + vnode.shapeFlag &= -257; + vnode.shapeFlag &= -513; + } + function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; + } + + function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup().` + (` If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` ) + ); + } + } + const createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } + }; + const onBeforeMount = createHook("bm"); + const onMounted = createHook("m"); + const onBeforeUpdate = createHook( + "bu" + ); + const onUpdated = createHook("u"); + const onBeforeUnmount = createHook( + "bum" + ); + const onUnmounted = createHook("um"); + const onServerPrefetch = createHook( + "sp" + ); + const onRenderTriggered = createHook("rtg"); + const onRenderTracked = createHook("rtc"); + function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); + } + + const COMPONENTS = "components"; + const DIRECTIVES = "directives"; + function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; + } + const NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc"); + function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } + } + function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); + } + function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } + } + function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); + } + + function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && isReactive(source); + let needsWrap = false; + let isReadonlySource = false; + if (sourceIsReactiveArray) { + needsWrap = !isShallow(source); + isReadonlySource = isReadonly(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i], + i, + void 0, + cached && cached[i] + ); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source)) { + warn$1(`The v-for range expect an integer value but got ${source}.`); + } + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; + } + + function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; + } + + function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + 64 + ); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; + } + function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; + } + + function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; + } + + const getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); + }; + const publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + /* @__PURE__ */ extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => shallowReadonly(i.props) , + $attrs: (i) => shallowReadonly(i.attrs) , + $slots: (i) => shallowReadonly(i.slots) , + $refs: (i) => shallowReadonly(i.refs) , + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => resolveMergedOptions(i) , + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => instanceWatch.bind(i) + }) + ); + const isReservedPrefix = (key) => key === "_" || key === "$"; + const hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); + const PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + let normalizedProps; + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1 /* SETUP */: + return setupState[key]; + case 2 /* DATA */: + return data[key]; + case 4 /* CONTEXT */: + return ctx[key]; + case 3 /* PROPS */: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1 /* SETUP */; + return setupState[key]; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2 /* DATA */; + return data[key]; + } else if ( + // only cache other properties when instance has declared (thus stable) + // props + (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key) + ) { + accessCache[key] = 3 /* PROPS */; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4 /* CONTEXT */; + return ctx[key]; + } else if (shouldCacheAccess) { + accessCache[key] = 0 /* OTHER */; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4 /* CONTEXT */; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate