com.bigdata.concurrent.TxDag Maven / Gradle / Ivy
/* Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 [email protected] This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.bigdata.concurrent; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Vector; import org.apache.log4j.Logger; /** *
and update the * closure of the WAITS_FOR graph. * * @param t * The index associated with a transaction (src WAITS_FOR dst). * @param u * The index associated with another transaction. * @param insert * True iff an edge is being inserted and false iff an edge is * being removed. * * @return true iff no deadlock was created, in which case M holds * the new state and t -> u should be added to the WAITS_FOR * graph. false iff a deadlock resulted, in which case ** Directed Acyclic Graph (DAG) for detecting and preventing deadlocks in a * concurrent programming system. The algorithm takes advantage of certain * characteristics of the deadlock detection problem for concurrent transactions * and provides a reasonable cost solution for that domain. The design uses a * boolean matrix W to code the edges in the WAITS_FOR graph and an integer * matrix M to code the the number of different paths between two vertices. * Operations that insert one or more edges are atomic -- if a deadlock would * result, then the state of the DAG is NOT changed (a deadlock is detected when * there is a non-zero path count in the diagonal of W). The cost of the * algorithm is less than
*O(n^^2)
and is suitable for systems * with a multi-programming level of 100s of concurrent transactions. ** This implementation is based on the online algorithm for deadlock detection * in Section 5, page 86 of: *
*Bayer, R. 1976. Integrity, Concurrency, and Recovery in Databases. In * Proceedings of the Proceedings of the 1st European Cooperation in informatics * on ECI Conference 1976 (August 09 - 12, 1976). K. Samelson, Ed. Lecture Notes * In Computer Science, vol. 44. Springer-Verlag, London, 79-106. *
* See the * citation online. ** Given that
* *w
is the directed acyclic graph of *WAITS_FOR
relation among the concurrent transactions. *w+
is the transitive closure ofw
. The online * algorithm solves the problem: ** Given w, w+, calculate * w', w'+ where * w' := w U {(ti,tj)} iff inserting an edge, or * w' := w / {(ti,tj)} iff removing an edge. ** ** The approach defines a matrix
*M[ t, u ]
whose cells are the * number of different paths fromt
tou
. A * deadlock is identified if the update algorithm forM
computes * a non-zero value for the diagonal. ** The update rules for M are as follows. "+/-" should be interpreted as "+" iff * an edge is being added and "-" iff an edge is being removed. The "." operator * is scalar multiplication. *
*
* Updates are made tentative using a secondary matrix, M2. The update is * applied to M2. If a deadlock would result, then the original matrix is not * modified. Otherwise the original matrix is replaced by M2. * *- M[s,v] := M[s,v] +/- M[s,t] . M[u,v]; s!=t; u!=v
*- M[s,u] := M[s,u] +/- M[s,t]; s!=t
*- M[t,v] := M[t,v] +/- M[u,v]; u!=v
*- M[t,u] := M[t,u] +/- 1
** The public interface is defined in terms of arbitrary objects designated by * the application as "transactions". The DAG is provisioned for a maximum * multi-programming level, which is used to dimension the internal matrices. * The choosen multi-programming level should correspond to the maximum multi- * programming level permitted, i.e., to the #of concurrent transactions which * may execute before subsequent requests for a new transaction are queued. * Internally the "transaction" objects are mapped using their hash code onto * pre-defined {@link Integer}s corresponding to indices in [0:n-1], where n is * the provisioned multi-programming level. *
*Usage notes
** This class is designed to be used internally by a class modeling a * {@link ResourceQueue}. Edges are added when a transaction must wait * for a resource on one or more transactions in the granted group for that * resource queue. Transactions are implicitly declared as they are referenced * when adding edges. The general case is that there are N transactions in the * granted group for some resource, so * {@link #addEdges(Object blocked, Object[] running)} would be used to indicate * that a transaction must wait on the granted group. *
** A transaction in a granted group is guarenteed to be running and hence not * waiting on any other transaction(s). When a transaction releases a lock, the * {@link ResourceQueue} automatically invokes * {@link #removeEdges(Object tx, boolean waiting)} with *
*waiting == false
in order to remove all WAITS_FOR * relationships whose target is that transaction. (The * {@link #removeEdges(Object, boolean)} method is optimized for the case when * it is known that a transaction is not waiting for any other transaction.) ** The integration layer MUST explicitly invoke * {@link #removeEdges(Object, boolean)} whenever a transaction commits or * aborts in order to remove all WAITS_FOR relationships involving that * transaction. Failure to do this will result in false reporting of deadlocks * since the transaction is still "on the books". The integration layer should * specify
* * @todo This implementation does not help the application to decide the minimum * "cost" set of transactions which would result in an acyclic graph if * their WAITS_FOR relationships (edges) were removed from the graph. This * could probably be achieved by an analysis of the path count matrix in * which the deadlock was detected combined with information about the * sunk cost of each transaction. * * @todo The use of DAGs for detecting and breaking deadlocks in support of * concurrent programming may require interaction with the locking * protocol. For example, a transaction requesting a lock which would * result in a deadlock may be moved up in the request queue for a lock if * that would resolve the deadlock. * * @todo Consider requiring explicitly registration of transactions. This is * parallel to the requirement for explicitly removal of transactions * using {@link #removeEdges(Object, boolean). * * @todo This class should probably be unsynchronized and should place the * burden for synchronization on the caller. * * @version $Id$ * @author Bryan Thompson */ public class TxDag { /** * Logger for this class. */ protected static final Logger log = Logger.getLogger(TxDag.class); protected static final boolean INFO = log.isInfoEnabled(); protected static final boolean DEBUG = log.isDebugEnabled(); /** * The maximum multi-programming level supported (from the constructor). */ private final int _capacity; /** * The asserted edges in the directed acyclic graph. W[u,v] is true iff u * WAITS_FOR v. */ private final boolean[][] W; /** * The #of different paths from u to v in {@link #W}. */ private final int[][] M; /** * A scratch buffer used to make conditional updates of M. * * @see #backup() */ private final int[][] M1; /** * The #of inbound edges for each transaction index. */ final int[] inbound; /** * The #of outbound edges for each transaction index. */ final int[] outbound; /** * An array of the application transaction objects in order by the indices * as assigned by {@link #lookup(Object, boolean)}. Entries in this array * are cleared (towaiting == false
iff it knows that the transaction was * NOT blocked. For example, a transaction which completes normally is never * blocked. However, if a decision is made to abort a blocked transaction, e.g., * owing to a timeout or external directive, then the caller MUST specify *waiting == true
and a less efficient technique will be used to * remove all edges involving the specificed transaction. *null
) when a vertex is removed from the * graph by {@link #releaseVertex(Object)}. */ final Object[] transactions; /** * Caches the results of the last {@link #getOrder()} call. */ private int[] _order = null; /** * An empty int[] used for order[] when the graph is empty. */ private final int[] EMPTY = new int[]{}; /** * This field controls whether or not the result of {@link #getOrder()} and * {@link #getOrder(int, int)} are sorted. Sorting is not required for * correctness, but sorting may make it easier to follow the behavior of the * algorithm. The default isfalse
. */ static public boolean sortOrder = false; /** * This field controls whether or not the order[] is cloned and then sorted * by {@link #toString()}. The default istrue
. */ static public boolean sortOrderForDisplay = true; /** * This field controls whether or not the result of {@link #getOrder()} is * cached. Caching is enabled by default but may be disabled for debugging. */ static public boolean cacheOrder = false; /** * The constant used by {@link #lookup(Object, boolean)} to indicate that * the named vertex was not found in the DAG (-1
). */ static public final int UNKNOWN = -1; /** * A list containing {@link Integer} indices available to be assigned to a * new transaction. When this list is empty, then the maximum #of * transactions are running concurrently. Entries are removed from the * list when they are assigned to a transaction. Entries are returned to * the list when a transaction is complete (abort or commit). */ private final Listindices = new LinkedList (); /** * Mapping from the application "transaction" object to the {@link Integer} * index assigned to that transaction. * * @see #indices */ private final Map M[t,t] > 0
for some t, indicating the presence * of a deadlock and M should be discarded. Note that * deadlock never results ifinsert == false
. * * @exception ArithmeticException * If the path count for any cell of the matrix would * overflow anint
. * * @see #backup(int[] order) * @see #restore(int[] order) */ final synchronized boolean updateClosure(final int t, final int u, final boolean insert) { /* * Note: Path counts can grow large quite quickly since they are * multiplicative. If you observe {@link ArithmeticException} being * thrown from this method under reasonable use cases then change the * definition of M from int[][] to long[][] and update the code below to * test for exceeding {@link Long#MAX_VALUE} rather than * {@link Integer#MAX_VALUE}. This will require changing parts of the * package private API, but it should not effect the public API. */ /* * Note: t, u are already indices into M. order[] is used to map the * other transactions into indices within M, but is NOT used with t and * u. Indices of M not found in order[] are unused at the time this * method is invoked. os := order[s]. ov := order[v]. * * Note: DO NOT write code like: for( int s=0, os=order[s]; ... ) -- it * produces the wrong behavior. */ final int[] order = ( insert ? getOrder(t,u) : getOrder() ); final int n = order.length; if( DEBUG ) { log.debug("W:: t("+t+") -> u("+u+"), insert="+insert+", size="+n); } final int max = Integer.MAX_VALUE; for( int s=0; smax ) throw new ArithmeticException("overflow"); M[os][ov] = (int)val; } else { M[os][ov] -= M[os][t] * M[u][ov]; } } // M[s,u] := M[s,u] +/- M[s,t]; s!=t if( DEBUG ) { log.debug("M[s="+os+",u="+u+"] := "+ "M[s="+os+",u="+u+"]("+M[os][u]+") +/- "+ "M[s="+os+",t="+t+"]("+M[os][t]+")" ); } if( insert ) { long val = M[os][u] + M[os][t]; if( val > max ) throw new ArithmeticException("overflow"); M[os][u] = (int) val; } else { M[os][u] -= M[os][t]; } } for( int v=0; v max ) throw new ArithmeticException("overflow"); M[t][ov] = (int) val; } else { M[t][ov] -= M[u][ov]; } } // M[t,u] := M[t,u] +/- 1 if( DEBUG ) { log.debug("M[t="+t+",u="+u+"] := "+ "M[t="+t+",u="+u+"]("+M[t][u]+") +/- 1" ); } if( insert ) { if( M[t][u] == max ) throw new ArithmeticException("overflow"); M[t][u] += 1; } else { M[t][u] -= 1; } // check for deadlock. for( int s=0; s 0 ) { if( DEBUG ) { log.debug("deadlock: M["+os+","+os+"]="+M[os][os]); } return false; } } return true; } /** * Returns a representation of the state of the graph suitable for debugging * the algorithm. */ synchronized public String toString() { final int[] order; if( sortOrderForDisplay) { /* * Copy and then sort order[]. We make a copy since this would * otherwise sort the cached order[], which would have side effects * that I want to avoid when debugging. */ order = (int[]) getOrder().clone(); Arrays.sort(order); // sort indices for display purposes. } else { // Get order[] -- may be cached. order = getOrder(); } StringBuffer sb = new StringBuffer(); // final int n = size(); sb.append("TxDag::\ncapacity="+capacity()+", size="+size()+"\n"); // get the in-use transaction indices into W and M. sb.append( "index\t#in\t#out\n"); for( int i=0; i "+transactions[oj]+"\n"); } } } /* * Matrix M. */ // column headings. sb.append("index"); for( int j=0; j 0 ) { deadlock = true; } } sb.append("\t"+transactions[oi]+"\n"); // row trailer } // column footers for( int j=0; j * Package private method returns a dense array containing a copy of the * in-use transaction indices that participate in at least one edge. * Transactions which have been declared to the {@link TxDag} but which have * neither inbound nor outbound edges are not reported. Such transactions * are neither waiting for other transactions nor being waited on by other * transactions and do not participate when computing the * {@link #updateClosure(int, int, boolean) closure} of W. By using only * those transactions that participate in at least one edge we reduce the * complexity of the closure update algorithm to an average complexity of * O((|W+|/n)^^2)
, where |W+| is the length of the returned * array. * * * @return A dense array of the transaction indices that also participate in * at least one edge. The indices may be present in any order and * the order may change from invocation to invocation -- even when * the state of the graph has not changed. * * @see #resetOrder() * @see #getOrder( int t, int u) */ synchronized int[] getOrder() { if ( cacheOrder && _order != null) { // return cached value. return _order; } // #of "in-use" transactions. final int n = size(); if (n == 0) { _order = EMPTY; return _order; } /* * Compute #of transactions actually used in at least one edge. We do * this by scanning the mapping and then verifying that each index in * turn serves as either the source or the target for at least one edge. */ final int[] tmp = new int[n]; int nnzero = 0; final Iterator itr = mapping.values().iterator(); while (itr.hasNext()) { final int index = ((Integer) itr.next()).intValue(); if( inbound[index]>0 || outbound[index]>0 ) { tmp[nnzero++] = index; } } if (nnzero == 0) { _order = EMPTY; return _order; } /* * Copy only the portion of the array that contains indices * corresponding to transactions that participate in at least one edge. */ _order = new int[nnzero]; System.arraycopy(tmp, 0, _order, 0, nnzero); /* * Note: sorting order[] aids debugging by ordering the behavior of * updateClosure(), but it is not required for correctness. */ if (sortOrder) { Arrays.sort(_order); } return _order; } /** * This is a special case version of {@link #getOrder()} that is invoked by * {@link #updateClosure(int t, int u, boolean insert)} when *insert == true
and forcest
and *u
to be included in the returned order[] even if those * vertices do not participate in any edges. In order to correctly update * the closure under insert, the order[] MUST containt
(u
) * even if that vertex does not currently participate in any edge since it * will participate after the edge has been added and therefore MUST * participdate in the matrix operations that update the closure of W. ** When
t
andu
both already participate in at * least one edge, then this method simply delegates to {@link #getOrder()}. * * @param t * A transaction index for which an edge is being added *t WAITS_FOR u
. * @param u * Another transaction index. * @return * * @see #updateClosure(int, int, boolean) * @see #getOrder() */ final synchronized int[] getOrder( final int t, final int u ) { if (t == u) { throw new IllegalArgumentException(); } if ((inbound[t] > 0 || outbound[t] > 0) && (inbound[u] > 0 || outbound[u] > 0)) { /* * Since both t and u are already participating in at least one edge * we can simply delegate this to {@link #getOrder()}. */ return getOrder(); } /* * Compute #of transactions actually used in at least one edge. We do * this by scanning the mapping and then verifying that each index in * turn serves as either the source or the target for at least one edge. * * Note: If the transaction index is either t or u then we force it to * be included. * * Note: We DO NOT update the _order field since that is only used to * cache based on the defacto state, not when we are actively inserting * an edge. */ final int n = size(); final int[] tmp = new int[n]; int nnzero = 0; final Iterator itr = mapping.values().iterator(); while (itr.hasNext()) { final int index = ((Integer) itr.next()).intValue(); if( index==t || index==u || inbound[index]>0 || outbound[index]>0 ) { tmp[nnzero++] = index; } } if (nnzero == 0) { return EMPTY; } /* * Copy only the portion of the array that contains indices * corresponding to transactions that participate in at least one edge. */ int[] order = new int[nnzero]; System.arraycopy(tmp, 0, order, 0, nnzero); /* * Note: sorting order[] aids debugging by ordering the behavior of * updateClosure(), but it is not required for correctness. */ if (sortOrder) { Arrays.sort(order); } return order; } /** * Resets the {@link #_order} cache so that {@link #getOrder()} will be * forced to recompute its response. This method is automatically. */ final synchronized void resetOrder() { _order = null; } /** * Add a set of edges to the DAG. Each edge has the semantics *blocked -> running[ i ]
, i.e., the blocked * transaction WAITS_FOR the running transaction. * * @param blocked * A transaction that is blocked waiting on one or more * transactions. * * @param running * One or more transactions in the granted group for some * resource. * * @exception IllegalArgumentException * If either argument isnull
. * @exception IllegalArgumentException * If any element of running isnull
. * @exception IllegalArgumentException * Ifblocked == running[ i]
for any element * of running . * @exception IllegalArgumentException * Ifrunning.length
is greater than the * capacity of the DAG. * @exception IllegalStateException * If creating the described edges would cause a duplicate * edge to be asserted (either the edge already exists or it * is defined more than once by the parameters). * @exception DeadlockException * If adding the described edges to the DAG would result in a * cycle. The state of the DAG is unchanged if this exception * is thrown. */ synchronized public void addEdges( final Object blocked, final Object[] running ) throws DeadlockException { if( running == blocked ) { throw new IllegalArgumentException("transaction may not wait for self"); } if( running == null ) { throw new IllegalArgumentException("running is null"); } if( running.length == 0 ) { return; // NOP. } // src of the edges. final int src = lookup( blocked, true ); // dst of the edges. final int[] dst= new int[ running.length ]; for( int i=0; itrue iff the described edge exists in the graph. * * @param blocked * A transaction. * @param running * A different transaction. * * @exception IllegalArgumentException * If either argument is null
. * @exception IllegalArgumentException * If the same transaction is specified for both arguments. * * @return true iff the edge exists. */ synchronized public boolean hasEdge( final Object blocked, final Object running ) { if( running == blocked ) { throw new IllegalArgumentException("transaction may not wait for itself"); } int dst = lookup( running, true ); int src = lookup( blocked, true ); if( src == dst ) { throw new IllegalArgumentException("transaction may not wait for itself"); } return W[src][dst]; } // /** // * Return an array of the application provided transaction objects. The // * index positions in this array correspond to the transaction indices as // * assigned by {@link #lookup(Object, boolean)}. The length of the array // * corresponds to the #of in-use transactions. // * // * @return An array of the in-use transaction objects that is indexed by the // * assigned transaction indices. // */ // // synchronized Object[] getTransactions() { // // extract index -> transaction object mapping. // final int n = size(); // final Object[] transactions = new Object[ n ]; // final Iterator itr = mapping.entrySet().iterator(); // while( itr.hasNext() ) { // final Map.Entry entry = (Map.Entry) itr.next(); // final int index = ((Integer)entry.getValue()).intValue(); // transactions[index] = entry.getKey(); // } // return transactions; // } /** * Return an array of the edges asserted for the graph. The length of the * array is the #of in use transactions in the graph. Each element of the * array represents a single edge. The state of the returned object is * current as of the time that this method executes and is not maintained. * * @return Return the edges of the graph. The edges are not in any * particular order. * * @see Edge */ synchronized public Edge[] getEdges( final boolean closure ) { final int[] order = getOrder(); final int n = order.length; // // extract index -> transaction object mapping. // Object[] transactions = getTransactions(); // populate array of explict edges w/ optional closure. Vectorv = new Vector (); for(int i=0; i 0 ) { v.add( new Edge( transactions[order[i]], transactions[order[j]], false ) ); } } } } return (Edge[]) v.toArray( new Edge[]{} ); } /** * A representation of an edge in the DAG used for export of information to * the caller. * * @author Bryan Thompson * */ public static class Edge { /** * The transaction object for the source vertex (src WAITS_FOR tgt). */ final public Object src; /** * The transaction object for the target vertex. */ final Object tgt; /** * True iff the edge was explicitly asserted and false if the edge * was inferred by the closure of the WAITS_FOR relationship. */ final boolean explicit; /** * @param src * @param tgt * @param explicit */ Edge( Object src, Object tgt, boolean explicit ) { if( src == null || tgt == null || src == tgt ) { throw new IllegalArgumentException(); } this.src = src; this.tgt = tgt; this.explicit = explicit; } /** * Human readable representation of the edge. */ public String toString() { return ""+src+" -> "+tgt+(explicit?"":" (inferred)"); } /** * The transaction object which is the source of the WAITS_FOR edge. */ public Object getSource() { return src; } /** * The transaction object which is the target of the WAITS_FOR edge. */ public Object getTarget() { return tgt; } /** * Return true iff the edge was explicitly asserted (versus implied * by the transitive closure of the WAITS_FOR graph). */ public boolean isExplicit() { return explicit; } } /** * Removes an edge from the DAG. * * Note: This method does NOT does not recycle the vertex associated with a * transaction which no longer has any incoming or outgoing edges. See * {@link #removeEdges(Object, boolean)}. * * @param blocked * A transaction which is currently waiting on running . * @param running * Another transaction. * * @exception IllegalArgumentException * If either argument is
null
. * @exception IllegalArgumentException * If the same transaction is specified for both arguments. * @exception IllegalStateException * If the described edge does not exist. */ synchronized public void removeEdge( final Object blocked, final Object running ) { final int src, tgt; if( ( src = lookup( blocked, false ) ) == -1 ) { throw new IllegalStateException("unknown transaction: tx1="+blocked); } if( ( tgt = lookup( running, false ) ) == -1 ) { throw new IllegalStateException("unknown transaction: tx2="+running); } if( src == tgt ) { throw new IllegalArgumentException(); } if( ! W[src][tgt] ) { throw new IllegalStateException("edge does not exist: src="+blocked+", tgt="+running); } /* * Note: This method does not preserve the internal state of the DAG * against an exception thrown by updateClosure. The reason for this is * that updateClosure should not be able to indicate a deadlock or cause * an ArithmeticException when removing edges. In order to be able to * protect against wild hairs we would have to backup M before removing * an edge, and that causes too much overhead for something which "should * not" happen. */ if( DEBUG ) { log.debug(toString()); } // update the closure. updateClosure( src, tgt, false ); // remove the edge. W[src][tgt] = false; outbound[src]--; inbound[tgt]--; if(outbound[src] == 0 || inbound[tgt] == 0 ) { resetOrder(); } if(outbound[src] <0 ) { throw new AssertionError(); } if( inbound[tgt] <0 ) { throw new AssertionError(); } if( DEBUG ) { log.debug(toString()); } } /** * Package private method removes an edge and updates the path count matrix * and the inbound and outbound edge counts for the source and target * vertices. The described edge must exist. * * @param src * The source vertex. * @param tgt * The target vertex. */ private synchronized void removeEdge( final int src, final int tgt ) { if( src == tgt ) { throw new IllegalArgumentException(); } if( ! W[src][tgt] ) { throw new IllegalStateException("edge does not exist: src="+src+", tgt="+tgt); } // update the closure. updateClosure( src, tgt, false ); // remove the edge. W[src][tgt] = false; outbound[src]--; inbound[tgt]--; if(outbound[src] == 0 || inbound[tgt] == 0 ) { resetOrder(); } if(outbound[src] <0 ) { throw new AssertionError(); } if( inbound[tgt] <0 ) { throw new AssertionError(); } } /** * Remove all edges whose target is tx. This method SHOULD be used * when a running transaction completes (either aborts or commits). After * calling this method, the transaction is removed completely from the DAG. * Failure to use this method will result in the capacity of the DAG being * consumed as vertices will not be recycled unless you call * {@link #releaseVertex(Object)}. * * @param tx * A transaction. * * @param waiting * When false, caller asserts that this transaction it is NOT * waiting on any other transaction. This assertion is used to * optimize the update of the path count matrix by simply * removing the row and column associated with this transaction. * When [waiting == true], a less efficient procedure is used to * update the path count matrix. ** Do NOT specify [waiting == false] unless you know * that the transaction is NOT waiting. In general, this * knowledge is available to the 2PL locking package. * * @todo Write test cases for this method. It duplicates much of the logic * of {@link #removeEdge(Object, Object)} and therefore must be * evaluated separately. */ synchronized public void removeEdges( final Object tx, final boolean waiting ) { final int tgt; if( ( tgt = lookup( tx, false ) ) == -1 ) { // No edges for that tx. return; // throw new IllegalStateException("unknown transaction: tx1="+tx); } if( DEBUG ) { log.debug(toString()); } if( ! waiting ) { /* * Clear the row and column for this transaction. This only visits * those transactions that have declared inbound or outbound edges. * We update the inbound and outbound edge counters for the vertices * that had edges involving [tgt] and clear the inbound and outbound * edge counters for the [tgt] vertex. */ final int[] order = getOrder(); for( int i=0; i
© 2015 - 2025 Weber Informatics LLC | Privacy Policy