diff options
author | Håkon Hallingstad <hakon@verizonmedia.com> | 2021-01-08 11:24:20 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@verizonmedia.com> | 2021-01-08 11:24:20 +0100 |
commit | d8fd5449d4e641dcea02368db8cf67253532981c (patch) | |
tree | 144ad6f87982d87f55891cbaa859b4ecd65f11ef /node-repository | |
parent | 97f39b069d9263cf1dfaf86aacc9c45de455bf31 (diff) |
Ensure fresh node with lock
Diffstat (limited to 'node-repository')
11 files changed, 135 insertions, 65 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeMutex.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeMutex.java new file mode 100644 index 00000000000..18ccdf0dd92 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeMutex.java @@ -0,0 +1,22 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision; + +import com.yahoo.transaction.Mutex; + +/** + * Holds a node and mutex pair, with the mutex closed by {@link #close()}. + * + * @author hakon + */ +public class NodeMutex implements Mutex { + private final Node node; + private final Mutex mutex; + + public NodeMutex(Node node, Mutex mutex) { + this.node = node; + this.mutex = mutex; + } + + public Node node() { return node; } + @Override public void close() { mutex.close(); } +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index b0e7c0bd61b..47f3b73024c 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -28,6 +28,7 @@ import com.yahoo.vespa.hosted.provision.maintenance.InfrastructureVersions; import com.yahoo.vespa.hosted.provision.maintenance.NodeFailer; import com.yahoo.vespa.hosted.provision.maintenance.PeriodicApplicationMaintainer; import com.yahoo.vespa.hosted.provision.node.Agent; +import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.NodeAcl; import com.yahoo.vespa.hosted.provision.node.filter.NodeFilter; @@ -53,6 +54,7 @@ import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -656,7 +658,8 @@ public class NodeRepository extends AbstractComponent { if (toState == Node.State.active && node.allocation().isEmpty()) illegal("Could not set " + node + " active. It has no allocation."); - try (Mutex lock = lock(node)) { + // TODO: Work out a safe lock acquisition strategy for moves, e.g. migrate to lockNode. + try (Mutex lock = lockOnly(node)) { if (toState == State.active) { for (Node currentActive : getNodes(node.allocation().get().owner(), State.active)) { if (node.allocation().get().membership().cluster().equals(currentActive.allocation().get().membership().cluster()) @@ -921,13 +924,57 @@ public class NodeRepository extends AbstractComponent { /** Create a lock which provides exclusive rights to modifying unallocated nodes */ public Mutex lockUnallocated() { return db.lockInactive(); } - /** Acquires the appropriate lock for this node */ - public Mutex lock(Node node) { + /** Returns a lock, and an up to date node fetched under an appropriate lock, if it exists. */ + public Optional<NodeMutex> lockNode(Node node) { + Node staleNode = node; + + for (int i = 0; i < 4; ++i) { + Mutex lock = lockOnly(staleNode); + Optional<Mutex> lockToClose = Optional.of(lock); + try { + Optional<Node> freshNode = getNode(staleNode.hostname(), staleNode.state()); + if (freshNode.isEmpty()) { + freshNode = getNode(staleNode.hostname()); + if (freshNode.isEmpty()) { + return Optional.empty(); + } + } + + if (Objects.equals(freshNode.get().allocation().map(Allocation::owner), + staleNode.allocation().map(Allocation::owner))) { + lockToClose = Optional.empty(); + return Optional.of(new NodeMutex(freshNode.get(), lock)); + } + + staleNode = freshNode.get(); + } finally { + lockToClose.ifPresent(Mutex::close); + } + } + + throw new IllegalStateException("Giving up trying to fetch an up to date node under lock: " + node.hostname()); + } + + /** Returns a lock, and an up to date node fetched under an appropriate lock, if it exists. */ + public Optional<NodeMutex> lockNode(String hostname) { + return getNode(hostname).flatMap(this::lockNode); + } + + /** Returns a lock, and an up to date node fetched under an appropriate lock. */ + public NodeMutex lockRequiredNode(Node node) { + return lockNode(node).orElseThrow(() -> new IllegalArgumentException("No such node: " + node.hostname())); + } + + /** Returns a lock, and an up to date node fetched under an appropriate lock. */ + public NodeMutex lockRequiredNode(String hostname) { + return lockNode(hostname).orElseThrow(() -> new IllegalArgumentException("No such node: " + hostname)); + } + + private Mutex lockOnly(Node node) { return node.allocation().isPresent() ? lock(node.allocation().get().owner()) : lockUnallocated(); } private void illegal(String message) { throw new IllegalArgumentException(message); } - } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java index 62d042d5e8b..b082b696ad4 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java @@ -9,6 +9,7 @@ import com.yahoo.config.provision.TransientException; import com.yahoo.jdisc.Metric; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.provision.Node; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.yolean.Exceptions; @@ -192,14 +193,15 @@ class MaintenanceDeployment implements Closeable { /** Returns true only if this operation changes the state of the wantToRetire flag */ private boolean markWantToRetire(Node node, boolean wantToRetire, Agent agent, NodeRepository nodeRepository) { - try (Mutex lock = nodeRepository.lock(node)) { - Optional<Node> nodeToMove = nodeRepository.getNode(node.hostname()); - if (nodeToMove.isEmpty()) return false; - if (nodeToMove.get().state() != Node.State.active) return false; + Optional<NodeMutex> nodeMutex = nodeRepository.lockNode(node); + if (nodeMutex.isEmpty()) return false; - if (nodeToMove.get().status().wantToRetire() == wantToRetire) return false; + try (var nodeLock = nodeMutex.get()) { + if (nodeLock.node().state() != Node.State.active) return false; - nodeRepository.write(nodeToMove.get().withWantToRetire(wantToRetire, agent, nodeRepository.clock().instant()), lock); + if (nodeLock.node().status().wantToRetire() == wantToRetire) return false; + + nodeRepository.write(nodeLock.node().withWantToRetire(wantToRetire, agent, nodeRepository.clock().instant()), nodeLock); return true; } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringUpgrader.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringUpgrader.java index 771f5e53e13..c8f4fc14dea 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringUpgrader.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringUpgrader.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.filter.NodeListFilter; @@ -61,10 +62,10 @@ public class RetiringUpgrader implements Upgrader { /** Retire and deprovision given host and its children */ private void retire(Node host, Version target, Instant now, NodeList allNodes) { if (!host.type().isHost()) throw new IllegalArgumentException("Cannot retire non-host " + host); - try (var lock = nodeRepository.lock(host)) { - Optional<Node> currentNode = nodeRepository.getNode(host.hostname()); - if (currentNode.isEmpty()) return; - host = currentNode.get(); + Optional<NodeMutex> nodeMutex = nodeRepository.lockNode(host); + if (nodeMutex.isEmpty()) return; + try (var lock = nodeMutex.get()) { + host = lock.node(); NodeType nodeType = host.type(); LOG.info("Retiring and deprovisioning " + host + ": On stale OS version " + diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java index 2b03a5cae8c..7d5d53c882e 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java @@ -8,9 +8,9 @@ import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.ParentHostUnavailableException; -import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.applications.ScalingEvent; @@ -126,13 +126,16 @@ class Activator { private void unreserveParentsOf(List<Node> nodes) { for (Node node : nodes) { if ( node.parentHostname().isEmpty()) continue; - Optional<Node> parent = nodeRepository.getNode(node.parentHostname().get()); + Optional<Node> parentNode = nodeRepository.getNode(node.parentHostname().get()); + if (parentNode.isEmpty()) continue; + if (parentNode.get().reservedTo().isEmpty()) continue; + + // Above is an optimization to avoid unnecessary locking - now repeat all conditions under lock + Optional<NodeMutex> parent = nodeRepository.lockNode(node.parentHostname().get()); if (parent.isEmpty()) continue; - if (parent.get().reservedTo().isEmpty()) continue; - try (Mutex lock = nodeRepository.lock(parent.get())) { - Optional<Node> lockedParent = nodeRepository.getNode(parent.get().hostname()); - if (lockedParent.isEmpty()) continue; - nodeRepository.write(lockedParent.get().withoutReservedTo(), lock); + try (var lock = parent.get()) { + if (lock.node().reservedTo().isEmpty()) continue; + nodeRepository.write(lock.node().withoutReservedTo(), lock); } } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 832310cf2c9..570ebdf767d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -86,8 +86,9 @@ public class NodePatcher { } if (RECURSIVE_FIELDS.contains(name)) { - for (Node child: patchedNodes.children()) + for (Node child: patchedNodes.children()) { patchedNodes.update(applyField(child, name, value, inspector, true)); + } } } ); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index c43629aeb09..9f8ba7d84a3 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -24,9 +24,9 @@ import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; -import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.provision.NoSuchNodeException; import com.yahoo.vespa.hosted.provision.Node; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.node.Address; @@ -160,14 +160,13 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { private HttpResponse handlePATCH(HttpRequest request) { String path = request.getUri().getPath(); if (path.startsWith("/nodes/v2/node/")) { - // TODO: Node is fetched outside of lock, might change after getting lock - Node node = nodeFromRequest(request); - try (var lock = nodeRepository.lock(node)) { - var patchedNodes = new NodePatcher(nodeFlavors, request.getData(), node, () -> nodeRepository.list(lock), - nodeRepository.clock()).apply(); + try (NodeMutex lock = nodeRepository.lockRequiredNode(nodeFromRequest(request))) { + var patchedNodes = new NodePatcher(nodeFlavors, request.getData(), lock.node(), () -> nodeRepository.list(lock), + nodeRepository.clock()).apply(); nodeRepository.write(patchedNodes, lock); + + return new MessageResponse("Updated " + lock.node().hostname()); } - return new MessageResponse("Updated " + node.hostname()); } else if (path.startsWith("/nodes/v2/upgrade/")) { return setTargetVersions(request); @@ -210,13 +209,11 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { } private HttpResponse deleteNode(String hostname) { - Optional<Node> node = nodeRepository.getNode(hostname); - if (node.isEmpty()) throw new NotFoundException("No node with hostname '" + hostname + "'"); - try (Mutex lock = nodeRepository.lock(node.get())) { - node = nodeRepository.getNode(hostname); - if (node.isEmpty()) throw new NotFoundException("No node with hostname '" + hostname + "'"); - if (node.get().state() == Node.State.deprovisioned) { - nodeRepository.forget(node.get()); + Optional<NodeMutex> nodeMutex = nodeRepository.lockNode(hostname); + if (nodeMutex.isEmpty()) throw new NotFoundException("No node with hostname '" + hostname + "'"); + try (var lock = nodeMutex.get()) { + if (lock.node().state() == Node.State.deprovisioned) { + nodeRepository.forget(lock.node()); return new MessageResponse("Permanently removed " + hostname); } else { List<Node> removedNodes = nodeRepository.removeRecursively(hostname); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockDeployer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockDeployer.java index 8099b08ae89..7cc170ce92b 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockDeployer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockDeployer.java @@ -11,9 +11,9 @@ import com.yahoo.config.provision.Deployer; import com.yahoo.config.provision.Deployment; import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.HostSpec; -import com.yahoo.transaction.Mutex; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.hosted.provision.Node; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.provisioning.NodeRepositoryProvisioner; @@ -196,8 +196,8 @@ public class MockDeployer implements Deployer { lastDeployTimes.put(applicationId, clock.instant()); for (Node node : nodeRepository.list().owner(applicationId).state(Node.State.active).wantToRetire().asList()) { - try (Mutex lock = nodeRepository.lock(node)) { - nodeRepository.write(node.retire(nodeRepository.clock().instant()), lock); + try (NodeMutex lock = nodeRepository.lockRequiredNode(node)) { + nodeRepository.write(lock.node().retire(nodeRepository.clock().instant()), lock); } } return redeployments++; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java index e6e19d0407e..668ff640304 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java @@ -182,18 +182,17 @@ public class NodeRepositoryTest { assertFalse(tester.nodeRepository().getNode("host1").get().history().hasEventAfter(History.Event.Type.deprovisioned, testStart)); // Set host 1 properties and deprovision it - Node host1 = tester.nodeRepository().getNode("host1").get(); - host1 = host1.withWantToRetire(true, true, Agent.system, tester.nodeRepository().clock().instant()); - host1 = host1.withFirmwareVerifiedAt(tester.clock().instant()); - host1 = host1.with(host1.status().withIncreasedFailCount()); - host1 = host1.with(host1.reports().withReport(Report.basicReport("id", Report.Type.HARD_FAIL, tester.clock().instant(), "Test report"))); - try (var lock = tester.nodeRepository().lock(host1)) { + try (var lock = tester.nodeRepository().lockRequiredNode("host1")) { + Node host1 = lock.node().withWantToRetire(true, true, Agent.system, tester.nodeRepository().clock().instant()); + host1 = host1.withFirmwareVerifiedAt(tester.clock().instant()); + host1 = host1.with(host1.status().withIncreasedFailCount()); + host1 = host1.with(host1.reports().withReport(Report.basicReport("id", Report.Type.HARD_FAIL, tester.clock().instant(), "Test report"))); tester.nodeRepository().write(host1, lock); } tester.nodeRepository().removeRecursively("host1"); // Host 1 is deprovisioned and unwanted properties are cleared - host1 = tester.nodeRepository().getNode("host1").get(); + Node host1 = tester.nodeRepository().getNode("host1").get(); assertEquals(Node.State.deprovisioned, host1.state()); assertTrue(host1.history().hasEventAfter(History.Event.Type.deprovisioned, testStart)); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java index 230195a92bd..f1a9ae7eae0 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java @@ -10,10 +10,10 @@ import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.OutOfCapacityException; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Zone; -import com.yahoo.transaction.Mutex; import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; +import com.yahoo.vespa.hosted.provision.NodeMutex; import com.yahoo.vespa.hosted.provision.node.Agent; import org.junit.Test; @@ -206,8 +206,8 @@ public class InPlaceResizeProvisionTest { // ... same with setting a node to want to retire Node nodeToWantoToRetire = listCluster(content1).not().retired().asList().get(0); - try (Mutex lock = tester.nodeRepository().lock(nodeToWantoToRetire)) { - tester.nodeRepository().write(nodeToWantoToRetire.withWantToRetire(true, Agent.system, + try (NodeMutex lock = tester.nodeRepository().lockRequiredNode(nodeToWantoToRetire)) { + tester.nodeRepository().write(lock.node().withWantToRetire(true, Agent.system, tester.clock().instant()), lock); } new PrepareHelper(tester, app).prepare(content1, 8, 1, halvedResources).activate(); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index 91c9f7e50ac..4adeab43842 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -25,7 +25,6 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provisioning.FlavorsConfig; import com.yahoo.test.ManualClock; -import com.yahoo.transaction.Mutex; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.mock.MockCurator; @@ -153,9 +152,8 @@ public class ProvisioningTester { public List<Node> patchNodes(List<Node> nodes, UnaryOperator<Node> patcher) { List<Node> updated = new ArrayList<>(); for (var node : nodes) { - try (var lock = nodeRepository.lock(node)) { - node = nodeRepository.getNode(node.hostname()).get(); - node = patcher.apply(node); + try (var lock = nodeRepository.lockRequiredNode(node)) { + node = patcher.apply(lock.node()); nodeRepository.write(node, lock); updated.add(node); } @@ -188,21 +186,21 @@ public class ProvisioningTester { // Add ip addresses and activate parent host if necessary for (HostSpec prepared : preparedNodes) { - Node node = nodeRepository.getNode(prepared.hostname()).get(); - if (node.ipConfig().primary().isEmpty()) { - node = node.with(new IP.Config(Set.of("::" + 0 + ":0"), Set.of())); - try (Mutex lock = nodeRepository.lock(node)) { + try (var lock = nodeRepository.lockRequiredNode(prepared.hostname())) { + Node node = lock.node(); + if (node.ipConfig().primary().isEmpty()) { + node = node.with(new IP.Config(Set.of("::" + 0 + ":0"), Set.of())); nodeRepository.write(node, lock); } + if (node.parentHostname().isEmpty()) continue; + Node parent = nodeRepository.getNode(node.parentHostname().get()).get(); + if (parent.state() == Node.State.active) continue; + NestedTransaction t = new NestedTransaction(); + if (parent.ipConfig().primary().isEmpty()) + parent = parent.with(new IP.Config(Set.of("::" + 0 + ":0"), Set.of("::" + 0 + ":2"))); + nodeRepository.activate(List.of(parent), t); + t.commit(); } - if (node.parentHostname().isEmpty()) continue; - Node parent = nodeRepository.getNode(node.parentHostname().get()).get(); - if (parent.state() == Node.State.active) continue; - NestedTransaction t = new NestedTransaction(); - if (parent.ipConfig().primary().isEmpty()) - parent = parent.with(new IP.Config(Set.of("::" + 0 + ":0"), Set.of("::" + 0 + ":2"))); - nodeRepository.activate(List.of(parent), t); - t.commit(); } return activate(application, preparedNodes); |