Repository: commons-dbcp Updated Branches: refs/heads/master ace3b46bf -> 70822f11d
[DBCP-514] Allow DBCP to register with a TransactionSynchronizationRegistry for XA cases. Apply modified patch that does not break binary compatibility. Bump up version from 2.5.1-SNAPSHOT to 2.6.0-SNAPSHOT since this patch adds new public APIs. Project: http://git-wip-us.apache.org/repos/asf/commons-dbcp/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-dbcp/commit/70822f11 Tree: http://git-wip-us.apache.org/repos/asf/commons-dbcp/tree/70822f11 Diff: http://git-wip-us.apache.org/repos/asf/commons-dbcp/diff/70822f11 Branch: refs/heads/master Commit: 70822f11d1a290a74f91fa688d73cf76650e3b4b Parents: ace3b46 Author: Gary Gregory <ggreg...@apache.org> Authored: Tue Jul 24 16:15:08 2018 -0600 Committer: Gary Gregory <ggreg...@apache.org> Committed: Tue Jul 24 16:15:08 2018 -0600 ---------------------------------------------------------------------- pom.xml | 2 +- src/changes/changes.xml | 7 +- .../dbcp2/managed/BasicManagedDataSource.java | 26 ++ .../managed/DataSourceXAConnectionFactory.java | 47 ++- .../dbcp2/managed/TransactionContext.java | 29 +- .../dbcp2/managed/TransactionRegistry.java | 18 +- .../dbcp2/managed/TestSynchronizationOrder.java | 305 +++++++++++++++++++ 7 files changed, 424 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index a36abbb..e6655e4 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ </parent> <modelVersion>4.0.0</modelVersion> <artifactId>commons-dbcp2</artifactId> - <version>2.5.1-SNAPSHOT</version> + <version>2.6.0-SNAPSHOT</version> <name>Apache Commons DBCP</name> <inceptionYear>2001</inceptionYear> http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index dbe133b..c924411 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -60,7 +60,12 @@ The <action> type attribute can be add,update,fix,remove. --> <body> - <release version="2.5.0" date="2018-MM-DD" description="This is a minor release, including bug fixes and enhancements."> + <release version="2.6.0" date="2018-MM-DD" description="This is a minor release, including bug fixes and enhancements."> + <action dev="ggregory" type="add" issue="DBCP-514" due-to="Gary Gregory"> + Allow DBCP to register with a TransactionSynchronizationRegistry for XA cases. + </action> + </release> + <release version="2.5.0" date="2018-07-15" description="This is a minor release, including bug fixes and enhancements."> <action dev="ggregory" type="update" issue="DBCP-505" due-to="Gary Gregory"> Update Java requirement from version 7 to 8. </action> http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java index 723c621..769fe5d 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java @@ -26,6 +26,7 @@ import org.apache.commons.dbcp2.PoolingDataSource; import javax.sql.DataSource; import javax.sql.XADataSource; import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; import java.sql.SQLException; @@ -62,6 +63,9 @@ public class BasicManagedDataSource extends BasicDataSource { /** XA data source instance */ private XADataSource xaDataSourceInstance; + /** Transaction Manager */ + private transient TransactionSynchronizationRegistry transactionSynchronizationRegistry; + /** * Gets the XADataSource instance used by the XAConnectionFactory. * @@ -99,6 +103,16 @@ public class BasicManagedDataSource extends BasicDataSource { } /** + * Gets the optional TransactionSynchronizationRegistry. + * + * @return the TSR that can be used to register synchronizations. + * @since 2.6.0 + */ + public TransactionSynchronizationRegistry getTransactionSynchronizationRegistry() { + return transactionSynchronizationRegistry; + } + + /** * Gets the transaction registry. * * @return the transaction registry associating XAResources with managed connections @@ -118,6 +132,18 @@ public class BasicManagedDataSource extends BasicDataSource { } /** + * Sets the optional TransactionSynchronizationRegistry property. + * + * @param transactionSynchronizationRegistry + * the TSR used to register synchronizations + * @since 2.6.0 + */ + public void setTransactionSynchronizationRegistry( + final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** * Gets the optional XADataSource class name. * * @return the optional XADataSource class name http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/main/java/org/apache/commons/dbcp2/managed/DataSourceXAConnectionFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/dbcp2/managed/DataSourceXAConnectionFactory.java b/src/main/java/org/apache/commons/dbcp2/managed/DataSourceXAConnectionFactory.java index 661ea62..a23fe0e 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/DataSourceXAConnectionFactory.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/DataSourceXAConnectionFactory.java @@ -23,6 +23,7 @@ import javax.sql.PooledConnection; import javax.sql.XAConnection; import javax.sql.XADataSource; import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; import javax.transaction.xa.XAResource; import org.apache.commons.dbcp2.Utils; @@ -50,9 +51,25 @@ public class DataSourceXAConnectionFactory implements XAConnectionFactory { * the transaction manager in which connections will be enlisted * @param xaDataSource * the data source from which connections will be retrieved + * @param transactionSynchronizationRegistry + * register with this TransactionSynchronizationRegistry + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { + this(transactionManager, xaDataSource, null, (char[]) null, transactionSynchronizationRegistry); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @since 2.6.0 */ public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource) { - this(transactionManager, xaDataSource, null, (char[]) null); + this(transactionManager, xaDataSource, null, (char[]) null, null); } /** @@ -70,9 +87,33 @@ public class DataSourceXAConnectionFactory implements XAConnectionFactory { */ public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, final String userName, final char[] userPassword) { + this(transactionManager, xaDataSource, userName, userPassword, null); + } + + /** + * Creates an DataSourceXAConnectionFactory which uses the specified XADataSource to create database connections. + * The connections are enlisted into transactions using the specified transaction manager. + * + * @param transactionManager + * the transaction manager in which connections will be enlisted + * @param xaDataSource + * the data source from which connections will be retrieved + * @param userName + * the user name used for authenticating new connections or null for unauthenticated + * @param userPassword + * the password used for authenticating new connections + * @param transactionSynchronizationRegistry + * register with this TransactionSynchronizationRegistry + * @since 2.6.0 + */ + public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, + final String userName, final char[] userPassword, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { Objects.requireNonNull(transactionManager, "transactionManager is null"); Objects.requireNonNull(xaDataSource, "xaDataSource is null"); - this.transactionRegistry = new TransactionRegistry(transactionManager); + + // We do allow the transactionSynchronizationRegistry to be null for non-app server environments + + this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry); this.xaDataSource = xaDataSource; this.userName = userName; this.userPassword = userPassword; @@ -93,7 +134,7 @@ public class DataSourceXAConnectionFactory implements XAConnectionFactory { */ public DataSourceXAConnectionFactory(final TransactionManager transactionManager, final XADataSource xaDataSource, final String userName, final String userPassword) { - this(transactionManager, xaDataSource, userName, Utils.toCharArray(userPassword)); + this(transactionManager, xaDataSource, userName, Utils.toCharArray(userPassword), null); } /** http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/main/java/org/apache/commons/dbcp2/managed/TransactionContext.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/dbcp2/managed/TransactionContext.java b/src/main/java/org/apache/commons/dbcp2/managed/TransactionContext.java index f4065d6..486ffcd 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/TransactionContext.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/TransactionContext.java @@ -22,6 +22,7 @@ import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.SystemException; import javax.transaction.Transaction; +import javax.transaction.TransactionSynchronizationRegistry; import javax.transaction.xa.XAResource; import java.sql.Connection; import java.sql.SQLException; @@ -38,6 +39,7 @@ import java.lang.ref.WeakReference; public class TransactionContext { private final TransactionRegistry transactionRegistry; private final WeakReference<Transaction> transactionRef; + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; private Connection sharedConnection; private boolean transactionComplete; @@ -49,13 +51,29 @@ public class TransactionContext { * the TransactionRegistry used to obtain the XAResource for the shared connection * @param transaction * the transaction + * @param transactionSynchronizationRegistry + * The optional TSR to register synchronizations with + * @since 2.6.0 */ - public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction) { + public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction, + final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { Objects.requireNonNull(transactionRegistry, "transactionRegistry is null"); Objects.requireNonNull(transaction, "transaction is null"); this.transactionRegistry = transactionRegistry; this.transactionRef = new WeakReference<>(transaction); this.transactionComplete = false; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** + * Provided for backwards compatability + * + * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the + * shared connection + * @param transaction the transaction + */ + public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction) { + this (transactionRegistry, transaction, null); } /** @@ -112,7 +130,7 @@ public class TransactionContext { */ public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException { try { - getTransaction().registerSynchronization(new Synchronization() { + final Synchronization s = new Synchronization() { @Override public void beforeCompletion() { // empty @@ -122,7 +140,12 @@ public class TransactionContext { public void afterCompletion(final int status) { listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED); } - }); + }; + if (transactionSynchronizationRegistry != null) { + transactionSynchronizationRegistry.registerInterposedSynchronization(s); + } else { + getTransaction().registerSynchronization(s); + } } catch (final RollbackException e) { // JTA spec doesn't let us register with a transaction marked rollback only // just ignore this and the tx state will be cleared another way. http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/main/java/org/apache/commons/dbcp2/managed/TransactionRegistry.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/dbcp2/managed/TransactionRegistry.java b/src/main/java/org/apache/commons/dbcp2/managed/TransactionRegistry.java index 250d6dc..794752d 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/TransactionRegistry.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/TransactionRegistry.java @@ -26,6 +26,7 @@ import java.util.WeakHashMap; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; import javax.transaction.xa.XAResource; import org.apache.commons.dbcp2.DelegatingConnection; @@ -43,15 +44,28 @@ public class TransactionRegistry { private final TransactionManager transactionManager; private final Map<Transaction, TransactionContext> caches = new WeakHashMap<>(); private final Map<Connection, XAResource> xaResources = new WeakHashMap<>(); + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; /** * Creates a TransactionRegistry for the specified transaction manager. * * @param transactionManager * the transaction manager used to enlist connections. + * @param transactionSynchronizationRegistry + * The optional TSR to register synchronizations with + * @since 2.6.0 */ - public TransactionRegistry(final TransactionManager transactionManager) { + public TransactionRegistry(final TransactionManager transactionManager, final TransactionSynchronizationRegistry transactionSynchronizationRegistry) { this.transactionManager = transactionManager; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + /** + * Provided for backwards compatability + * @param transactionManager the transaction manager used to enlist connections + */ + public TransactionRegistry(final TransactionManager transactionManager) { + this (transactionManager, null); } /** @@ -115,7 +129,7 @@ public class TransactionRegistry { synchronized (this) { TransactionContext cache = caches.get(transaction); if (cache == null) { - cache = new TransactionContext(this, transaction); + cache = new TransactionContext(this, transaction, transactionSynchronizationRegistry); caches.put(transaction, cache); } return cache; http://git-wip-us.apache.org/repos/asf/commons-dbcp/blob/70822f11/src/test/java/org/apache/commons/dbcp2/managed/TestSynchronizationOrder.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/dbcp2/managed/TestSynchronizationOrder.java b/src/test/java/org/apache/commons/dbcp2/managed/TestSynchronizationOrder.java new file mode 100644 index 0000000..ae582f5 --- /dev/null +++ b/src/test/java/org/apache/commons/dbcp2/managed/TestSynchronizationOrder.java @@ -0,0 +1,305 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.dbcp2.managed; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.dbcp2.DelegatingConnection; +import org.apache.commons.dbcp2.PoolableConnectionFactory; +import org.apache.commons.dbcp2.TesterClassLoader; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; +import javax.transaction.xa.XAResource; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; + +public class TestSynchronizationOrder { + + private boolean transactionManagerRegistered; + private boolean transactionSynchronizationRegistryRegistered; + private TransactionManager transactionManager; + private TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private XADataSource xads; + private BasicManagedDataSource bmds; + private BasicDataSource bds; + + @Test + public void testSessionSynchronization() throws Exception { + final DataSourceXAConnectionFactory xaConnectionFactory = new DataSourceXAConnectionFactory(transactionManager, xads); + + final PoolableConnectionFactory factory = + new PoolableConnectionFactory(xaConnectionFactory, null); + factory.setValidationQuery("SELECT DUMMY FROM DUAL"); + factory.setDefaultReadOnly(Boolean.TRUE); + factory.setDefaultAutoCommit(Boolean.TRUE); + + // create the pool + final GenericObjectPool pool = new GenericObjectPool<>(factory); + factory.setPool(pool); + pool.setMaxTotal(10); + pool.setMaxWaitMillis(1000); + + // finally create the datasource + final ManagedDataSource ds = new ManagedDataSource<>(pool, xaConnectionFactory.getTransactionRegistry()); + ds.setAccessToUnderlyingConnectionAllowed(true); + + + transactionManager.begin(); + final DelegatingConnection<?> connectionA = (DelegatingConnection<?>) ds.getConnection(); + connectionA.close(); + transactionManager.commit(); + assertTrue(transactionManagerRegistered); + assertFalse(transactionSynchronizationRegistryRegistered); + } + + @Test + public void testInterposedSynchronization() throws Exception { + final DataSourceXAConnectionFactory xaConnectionFactory = new DataSourceXAConnectionFactory(transactionManager, xads, transactionSynchronizationRegistry); + + final PoolableConnectionFactory factory = + new PoolableConnectionFactory(xaConnectionFactory, null); + factory.setValidationQuery("SELECT DUMMY FROM DUAL"); + factory.setDefaultReadOnly(Boolean.TRUE); + factory.setDefaultAutoCommit(Boolean.TRUE); + + // create the pool + final GenericObjectPool pool = new GenericObjectPool<>(factory); + factory.setPool(pool); + pool.setMaxTotal(10); + pool.setMaxWaitMillis(1000); + + // finally create the datasource + final ManagedDataSource ds = new ManagedDataSource<>(pool, xaConnectionFactory.getTransactionRegistry()); + ds.setAccessToUnderlyingConnectionAllowed(true); + + + transactionManager.begin(); + final DelegatingConnection<?> connectionA = (DelegatingConnection<?>) ds.getConnection(); + connectionA.close(); + transactionManager.commit(); + assertFalse(transactionManagerRegistered); + assertTrue(transactionSynchronizationRegistryRegistered); + } + + @After + public void tearDown() throws SQLException { + bds.close(); + bmds.close(); + } + + @Before + public void setup() { + transactionManager = new TransactionManager() { + + @Override + public void begin() throws NotSupportedException, SystemException { + + } + + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException, RollbackException, SecurityException, SystemException { + + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public Transaction getTransaction() throws SystemException { + return new Transaction() { + + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException, SecurityException, SystemException { + + } + + @Override + public boolean delistResource(final XAResource xaResource, final int i) throws IllegalStateException, SystemException { + return false; + } + + @Override + public boolean enlistResource(final XAResource xaResource) throws IllegalStateException, RollbackException, SystemException { + // Called and used + return true; + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public void registerSynchronization(final Synchronization synchronization) throws IllegalStateException, RollbackException, SystemException { + transactionManagerRegistered = true; + } + + @Override + public void rollback() throws IllegalStateException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + }; + } + + @Override + public void resume(final Transaction transaction) throws IllegalStateException, InvalidTransactionException, SystemException { + + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + + @Override + public void setTransactionTimeout(final int i) throws SystemException { + + } + + @Override + public Transaction suspend() throws SystemException { + return null; + } + }; + + transactionSynchronizationRegistry = new TransactionSynchronizationRegistry() { + + @Override + public Object getResource(final Object o) { + return null; + } + + @Override + public boolean getRollbackOnly() { + return false; + } + + @Override + public Object getTransactionKey() { + return null; + } + + @Override + public int getTransactionStatus() { + return 0; + } + + @Override + public void putResource(final Object o, final Object o1) { + + } + + @Override + public void registerInterposedSynchronization(final Synchronization synchronization) { + transactionSynchronizationRegistryRegistered = true; + } + + @Override + public void setRollbackOnly() { + + } + }; + + bmds = new BasicManagedDataSource(); + bmds.setTransactionManager(transactionManager); + bmds.setTransactionSynchronizationRegistry(transactionSynchronizationRegistry); + bmds.setXADataSource("notnull"); + bds = new BasicDataSource(); + bds.setDriverClassName("org.apache.commons.dbcp2.TesterDriver"); + bds.setUrl("jdbc:apache:commons:testdriver"); + bds.setMaxTotal(10); + bds.setMaxWaitMillis(100L); + bds.setDefaultAutoCommit(Boolean.TRUE); + bds.setDefaultReadOnly(Boolean.FALSE); + bds.setDefaultTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + bds.setDefaultCatalog("test catalog"); + bds.setUsername("userName"); + bds.setPassword("password"); + bds.setValidationQuery("SELECT DUMMY FROM DUAL"); + bds.setConnectionInitSqls(Arrays.asList(new String[]{"SELECT 1", "SELECT 2"})); + bds.setDriverClassLoader(new TesterClassLoader()); + bds.setJmxName("org.apache.commons.dbcp2:name=test"); + final AtomicInteger closeCounter = new AtomicInteger(); + final InvocationHandler handle = new InvocationHandler() { + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + final String methodName = method.getName(); + if (methodName.equals("hashCode")) { + return Integer.valueOf(System.identityHashCode(proxy)); + } + if (methodName.equals("equals")) { + return Boolean.valueOf(proxy == args[0]); + } + if (methodName.equals("getXAConnection")) { + // both zero and 2-arg signatures + return getXAConnection(); + } + try { + return method.invoke(bds, args); + } catch (final InvocationTargetException e) { + throw e.getTargetException(); + } + } + + protected XAConnection getXAConnection() throws SQLException { + return new TesterBasicXAConnection(bds.getConnection(), closeCounter); + } + }; + xads = (XADataSource) Proxy.newProxyInstance( + InvocationHandler.class.getClassLoader(), + new Class[]{XADataSource.class}, handle); + bmds.setXaDataSourceInstance(xads); + + } +}