HHH-6736 Added support for SELECT ... FOR UPDATE SKIP LOCKED

This commit is contained in:
Aleksander Blomskøld 2013-01-27 18:58:35 +01:00 committed by Steve Ebersole
parent 59bb86978e
commit e0cfc6bf2e
17 changed files with 189 additions and 88 deletions

View File

@ -263,6 +263,11 @@
<entry><para>acquired upon explicit user request using a <code>SELECT ... FOR UPDATE NOWAIT</code> in
Oracle.</para></entry>
</row>
<row>
<entry>LockMode.UPGRADE_SKIPLOCKED</entry>
<entry><para>acquired upon explicit user request using a <code>SELECT ... FOR UPDATE SKIP LOCKED</code> in
Oracle, or <code>SELECT ... with (rowlock,updlock,readpast) in SQL Server</code>.</para></entry>
</row>
<row>
<entry>LockMode.READ</entry>
<entry><para>acquired automatically when Hibernate reads data under <phrase>Repeatable Read</phrase> or
@ -299,17 +304,18 @@
</listitem>
</itemizedlist>
<para>
If you call <methodname>Session.load()</methodname> with option <option>UPGRADE</option> or
<option>UPGRADE_NOWAIT</option>, and the requested object is not already loaded by the session, the object is
loaded using <code>SELECT ... FOR UPDATE</code>. If you call <methodname>load()</methodname> for an object that
is already loaded with a less restrictive lock than the one you request, Hibernate calls
<methodname>lock()</methodname> for that object.
If you call <methodname>Session.load()</methodname> with option <option>UPGRADE</option>,
<option>UPGRADE_NOWAIT</option> or <option>UPGRADE_SKIPLOCKED</option>, and the requested object is not already
loaded by the session, the object is loaded using <code>SELECT ... FOR UPDATE</code>. If you call
<methodname>load()</methodname> for an object that is already loaded with a less restrictive lock than the one
you request, Hibernate calls <methodname>lock()</methodname> for that object.
</para>
<para>
<methodname>Session.lock()</methodname> performs a version number check if the specified lock mode is
<literal>READ</literal>, <literal>UPGRADE</literal>, or <literal>UPGRADE_NOWAIT</literal>. In the case of
<literal>UPGRADE</literal> or <literal>UPGRADE_NOWAIT</literal>, <code>SELECT ... FOR UPDATE</code> syntax is
used.
<literal>READ</literal>, <literal>UPGRADE</literal>, <literal>UPGRADE_NOWAIT</literal> or
<literal>UPGRADE_SKIPLOCKED</literal>. In the case of <literal>UPGRADE</literal>,
<literal>UPGRADE_NOWAIT</literal> or <literal>UPGRADE_SKIPLOCKED</literal>, <code>SELECT ... FOR UPDATE</code>
syntax is used.
</para>
<para>
If the requested lock mode is not supported by the database, Hibernate uses an appropriate alternate mode

View File

@ -66,6 +66,15 @@ public enum LockMode {
* <tt>UPGRADE</tt>.
*/
UPGRADE_NOWAIT( 10 ),
/**
* Attempt to obtain an upgrade lock, using an Oracle-style
* <tt>select for update skip locked</tt>. The semantics of
* this lock mode, once obtained, are the same as
* <tt>UPGRADE</tt>.
*/
UPGRADE_SKIPLOCKED( 10 ),
/**
* A <tt>WRITE</tt> lock is obtained when an object is updated
* or inserted. This lock mode is for internal use only and is

View File

@ -64,6 +64,12 @@ public class LockOptions implements Serializable {
*/
public static final int WAIT_FOREVER = -1;
/**
* Indicates that rows that are already locked should be skipped.
* @see #getTimeOut()
*/
public static final int SKIP_LOCKED = -2;
private LockMode lockMode = LockMode.NONE;
private int timeout = WAIT_FOREVER;
@ -221,9 +227,9 @@ public class LockOptions implements Serializable {
* The timeout is the amount of time, in milliseconds, we should instruct the database
* to wait for any requested pessimistic lock acquisition.
* <p/>
* {@link #NO_WAIT} and {@link #WAIT_FOREVER} represent 2 "magic" values.
* {@link #NO_WAIT}, {@link #WAIT_FOREVER} or {@link #SKIP_LOCKED} represent 3 "magic" values.
*
* @return timeout in milliseconds, or {@link #NO_WAIT} or {@link #WAIT_FOREVER}
* @return timeout in milliseconds, {@link #NO_WAIT}, {@link #WAIT_FOREVER} or {@link #SKIP_LOCKED}
*/
public int getTimeOut() {
return timeout;

View File

@ -375,6 +375,9 @@ public abstract class ResultSetMappingBinder {
else if ( "upgrade-nowait".equals( lockMode ) ) {
return LockMode.UPGRADE_NOWAIT;
}
else if ( "upgrade-skiplocked".equals( lockMode )) {
return LockMode.UPGRADE_SKIPLOCKED;
}
else if ( "write".equals( lockMode ) ) {
return LockMode.WRITE;
}

View File

@ -23,40 +23,14 @@
*/
package org.hibernate.dialect;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.NClob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.NullPrecedence;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.function.CastFunction;
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.dialect.function.StandardAnsiSqlAggregationFunctions;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.dialect.lock.LockingStrategy;
import org.hibernate.dialect.lock.OptimisticForceIncrementLockingStrategy;
import org.hibernate.dialect.lock.OptimisticLockingStrategy;
import org.hibernate.dialect.lock.PessimisticForceIncrementLockingStrategy;
import org.hibernate.dialect.lock.PessimisticReadSelectLockingStrategy;
import org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy;
import org.hibernate.dialect.lock.SelectLockingStrategy;
import org.hibernate.dialect.function.*;
import org.hibernate.dialect.lock.*;
import org.hibernate.dialect.pagination.LegacyLimitHandler;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.unique.DefaultUniqueDelegate;
@ -78,16 +52,17 @@ import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.io.StreamCopier;
import org.hibernate.mapping.Column;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.sql.ANSICaseFragment;
import org.hibernate.sql.ANSIJoinFragment;
import org.hibernate.sql.CaseFragment;
import org.hibernate.sql.ForUpdateFragment;
import org.hibernate.sql.JoinFragment;
import org.hibernate.sql.*;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.sql.ClobTypeDescriptor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
import org.jboss.logging.Logger;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.*;
import java.util.*;
/**
* Represents a dialect of SQL implemented by a particular RDBMS.
* Subclasses implement Hibernate compatibility with different systems.<br>
@ -1195,6 +1170,8 @@ public abstract class Dialect implements ConversionContext {
case FORCE:
case PESSIMISTIC_FORCE_INCREMENT:
return getForUpdateNowaitString();
case UPGRADE_SKIPLOCKED:
return getForUpdateSkipLockedString();
default:
return "";
}
@ -1313,6 +1290,16 @@ public abstract class Dialect implements ConversionContext {
return getForUpdateString();
}
/**
* Retrieves the <tt>FOR UPDATE SKIP LOCKED</tt> syntax specific to this dialect.
*
* @return The appropriate <tt>FOR UPDATE SKIP LOCKED</tt> clause string.
*/
public String getForUpdateSkipLockedString() {
// by default we report no support for SKIP_LOCKED lock semantics
return getForUpdateString();
}
/**
* Get the <tt>FOR UPDATE OF column_list NOWAIT</tt> fragment appropriate
* for this dialect given the aliases of the columns to be write locked.
@ -1324,6 +1311,17 @@ public abstract class Dialect implements ConversionContext {
return getForUpdateString( aliases );
}
/**
* Get the <tt>FOR UPDATE OF column_list SKIP LOCKED</tt> fragment appropriate
* for this dialect given the aliases of the columns to be write locked.
*
* @param aliases The columns to be write locked.
* @return The appropriate <tt>FOR UPDATE colunm_list SKIP LOCKED</tt> clause string.
*/
public String getForUpdateSkipLockedString(String aliases) {
return getForUpdateString( aliases );
}
/**
* Some dialects support an alternative means to <tt>SELECT FOR UPDATE</tt>,
* whereby a "lock hint" is appends to the table name in the from clause.
@ -2427,7 +2425,7 @@ public abstract class Dialect implements ConversionContext {
public UniqueDelegate getUniqueDelegate() {
return uniqueDelegate;
}
public String getNotExpression( String expression ) {
return "not " + expression;
}

View File

@ -22,6 +22,7 @@
* Boston, MA 02110-1301 USA
*/
package org.hibernate.dialect;
import org.hibernate.LockOptions;
import org.hibernate.sql.ANSIJoinFragment;
import org.hibernate.sql.JoinFragment;
@ -45,4 +46,21 @@ public class Oracle10gDialect extends Oracle9iDialect {
public JoinFragment createOuterJoinFragment() {
return new ANSIJoinFragment();
}
public String getWriteLockString(int timeout) {
if ( timeout == LockOptions.SKIP_LOCKED ) {
return getForUpdateSkipLockedString();
}
else {
return super.getWriteLockString(timeout);
}
}
public String getForUpdateSkipLockedString() {
return " for update skip locked";
}
public String getForUpdateSkipLockedString(String aliases) {
return getForUpdateString() + " of " + aliases + " skip locked";
}
}

View File

@ -134,6 +134,8 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
return tableName + " with (updlock, rowlock)";
case PESSIMISTIC_READ:
return tableName + " with (holdlock, rowlock)";
case UPGRADE_SKIPLOCKED:
return tableName + " with (updlock, rowlock, readpast)";
default:
return tableName;
}

View File

@ -54,11 +54,18 @@ public abstract class AbstractSelectLockingStrategy implements LockingStrategy {
protected abstract String generateLockString(int lockTimeout);
protected String determineSql(int timeout) {
return timeout == LockOptions.WAIT_FOREVER
? waitForeverSql
: timeout == LockOptions.NO_WAIT
? getNoWaitSql()
: generateLockString( timeout );
if ( timeout == LockOptions.WAIT_FOREVER) {
return waitForeverSql;
}
else if ( timeout == LockOptions.NO_WAIT) {
return getNoWaitSql();
}
else if ( timeout == LockOptions.SKIP_LOCKED) {
return getSkipLockedSql();
}
else {
return generateLockString( timeout );
}
}
private String noWaitSql;
@ -69,4 +76,13 @@ public abstract class AbstractSelectLockingStrategy implements LockingStrategy {
}
return noWaitSql;
}
private String skipLockedSql;
public String getSkipLockedSql() {
if ( skipLockedSql == null ) {
skipLockedSql = generateLockString( LockOptions.SKIP_LOCKED );
}
return skipLockedSql;
}
}

View File

@ -57,7 +57,8 @@ public class LockModeConverter {
}
else if ( lockMode == LockMode.PESSIMISTIC_WRITE
|| lockMode == LockMode.UPGRADE
|| lockMode == LockMode.UPGRADE_NOWAIT ) {
|| lockMode == LockMode.UPGRADE_NOWAIT
|| lockMode == LockMode.UPGRADE_SKIPLOCKED) {
return LockModeType.PESSIMISTIC_WRITE;
}
else if ( lockMode == LockMode.PESSIMISTIC_FORCE_INCREMENT

View File

@ -254,22 +254,24 @@ public abstract class Loader {
Dialect dialect,
List<AfterLoadAction> afterLoadActions) {
if ( dialect.useFollowOnLocking() ) {
LOG.usingFollowOnLocking();
// currently only one lock mode is allowed in follow-on locking
final LockMode lockMode = determineFollowOnLockMode( parameters.getLockOptions() );
final LockOptions lockOptions = new LockOptions( lockMode );
lockOptions.setTimeOut( parameters.getLockOptions().getTimeOut() );
lockOptions.setScope( parameters.getLockOptions().getScope() );
afterLoadActions.add(
new AfterLoadAction() {
@Override
public void afterLoad(SessionImplementor session, Object entity, Loadable persister) {
( (Session) session ).buildLockRequest( lockOptions ).lock( persister.getEntityName(), entity );
if ( lockOptions.getLockMode() != LockMode.UPGRADE_SKIPLOCKED ) {
LOG.usingFollowOnLocking();
lockOptions.setTimeOut( parameters.getLockOptions().getTimeOut() );
lockOptions.setScope( parameters.getLockOptions().getScope() );
afterLoadActions.add(
new AfterLoadAction() {
@Override
public void afterLoad(SessionImplementor session, Object entity, Loadable persister) {
( (Session) session ).buildLockRequest( lockOptions ).lock( persister.getEntityName(), entity );
}
}
}
);
parameters.setLockOptions( new LockOptions() );
return true;
);
parameters.setLockOptions( new LockOptions() );
return true;
}
}
return false;
}

View File

@ -205,27 +205,28 @@ public class CriteriaLoader extends OuterJoinLoader {
}
if ( dialect.useFollowOnLocking() ) {
// Dialect prefers to perform locking in a separate step
LOG.usingFollowOnLocking();
final LockMode lockMode = determineFollowOnLockMode( lockOptions );
if( lockMode != LockMode.UPGRADE_SKIPLOCKED ) {
// Dialect prefers to perform locking in a separate step
LOG.usingFollowOnLocking();
final LockMode lockMode = determineFollowOnLockMode( lockOptions );
final LockOptions lockOptionsToUse = new LockOptions( lockMode );
lockOptionsToUse.setTimeOut( lockOptions.getTimeOut() );
lockOptionsToUse.setScope( lockOptions.getScope() );
final LockOptions lockOptionsToUse = new LockOptions( lockMode );
lockOptionsToUse.setTimeOut( lockOptions.getTimeOut() );
lockOptionsToUse.setScope( lockOptions.getScope() );
afterLoadActions.add(
new AfterLoadAction() {
@Override
public void afterLoad(SessionImplementor session, Object entity, Loadable persister) {
( (Session) session ).buildLockRequest( lockOptionsToUse )
.lock( persister.getEntityName(), entity );
}
}
);
parameters.setLockOptions( new LockOptions() );
return sql;
afterLoadActions.add(
new AfterLoadAction() {
@Override
public void afterLoad(SessionImplementor session, Object entity, Loadable persister) {
( (Session) session ).buildLockRequest( lockOptionsToUse )
.lock( persister.getEntityName(), entity );
}
}
);
parameters.setLockOptions( new LockOptions() );
return sql;
}
}
final LockOptions locks = new LockOptions(lockOptions.getLockMode());
locks.setScope( lockOptions.getScope());
locks.setTimeOut( lockOptions.getTimeOut());

View File

@ -1910,6 +1910,7 @@ public abstract class AbstractEntityPersister
lockers.put( LockMode.READ, generateLocker( LockMode.READ ) );
lockers.put( LockMode.UPGRADE, generateLocker( LockMode.UPGRADE ) );
lockers.put( LockMode.UPGRADE_NOWAIT, generateLocker( LockMode.UPGRADE_NOWAIT ) );
lockers.put( LockMode.UPGRADE_SKIPLOCKED, generateLocker( LockMode.UPGRADE_SKIPLOCKED ) );
lockers.put( LockMode.FORCE, generateLocker( LockMode.FORCE ) );
lockers.put( LockMode.PESSIMISTIC_READ, generateLocker( LockMode.PESSIMISTIC_READ ) );
lockers.put( LockMode.PESSIMISTIC_WRITE, generateLocker( LockMode.PESSIMISTIC_WRITE ) );
@ -3851,6 +3852,12 @@ public abstract class AbstractEntityPersister
readLoader :
createEntityLoader( LockMode.UPGRADE_NOWAIT )
);
loaders.put(
LockMode.UPGRADE_SKIPLOCKED,
disableForUpdate ?
readLoader :
createEntityLoader( LockMode.UPGRADE_SKIPLOCKED )
);
loaders.put(
LockMode.FORCE,
disableForUpdate ?

View File

@ -39,6 +39,7 @@ import org.hibernate.internal.util.StringHelper;
public class ForUpdateFragment {
private final StringBuilder aliases = new StringBuilder();
private boolean isNowaitEnabled;
private boolean isSkipLockedEnabled;
private final Dialect dialect;
private LockMode lockMode;
private LockOptions lockOptions;
@ -89,6 +90,10 @@ public class ForUpdateFragment {
if ( upgradeType == LockMode.UPGRADE_NOWAIT ) {
setNowaitEnabled( true );
}
if ( upgradeType == LockMode.UPGRADE_SKIPLOCKED ) {
setSkipLockedEnabled( true );
}
}
public ForUpdateFragment addTableAlias(String alias) {
@ -110,13 +115,25 @@ public class ForUpdateFragment {
return "";
}
// TODO: pass lockmode
return isNowaitEnabled ?
dialect.getForUpdateNowaitString( aliases.toString() ) :
dialect.getForUpdateString( aliases.toString() );
if(isNowaitEnabled) {
return dialect.getForUpdateNowaitString( aliases.toString() );
}
else if (isSkipLockedEnabled) {
return dialect.getForUpdateSkipLockedString( aliases.toString() );
}
else {
return dialect.getForUpdateString( aliases.toString() );
}
}
public ForUpdateFragment setNowaitEnabled(boolean nowait) {
isNowaitEnabled = nowait;
return this;
}
public ForUpdateFragment setSkipLockedEnabled(boolean skipLocked) {
isSkipLockedEnabled = skipLocked;
return this;
}
}

View File

@ -993,7 +993,7 @@ finder methods for named queries -->
<!ATTLIST return alias CDATA #IMPLIED>
<!ATTLIST return entity-name CDATA #IMPLIED>
<!ATTLIST return class CDATA #IMPLIED>
<!ATTLIST return lock-mode (none|read|upgrade|upgrade-nowait|write) "read">
<!ATTLIST return lock-mode (none|read|upgrade|upgrade-nowait|upgrade-skiplocked|write) "read">
<!ELEMENT return-property (return-column*)>
<!ATTLIST return-property name CDATA #REQUIRED>
@ -1008,12 +1008,12 @@ finder methods for named queries -->
<!ELEMENT return-join (return-property)*>
<!ATTLIST return-join alias CDATA #REQUIRED>
<!ATTLIST return-join property CDATA #REQUIRED>
<!ATTLIST return-join lock-mode (none|read|upgrade|upgrade-nowait|write) "read">
<!ATTLIST return-join lock-mode (none|read|upgrade|upgrade-nowait|upgrade-skiplocked|write) "read">
<!ELEMENT load-collection (return-property)*>
<!ATTLIST load-collection alias CDATA #REQUIRED>
<!ATTLIST load-collection role CDATA #REQUIRED>
<!ATTLIST load-collection lock-mode (none|read|upgrade|upgrade-nowait|write) "read">
<!ATTLIST load-collection lock-mode (none|read|upgrade|upgrade-nowait|upgrade-skiplocked|write) "read">
<!ELEMENT return-scalar EMPTY>
<!ATTLIST return-scalar column CDATA #REQUIRED>

View File

@ -1785,6 +1785,7 @@ arbitrary number of queries, and import declarations of arbitrary classes.
<xs:enumeration value="read"/>
<xs:enumeration value="upgrade"/>
<xs:enumeration value="upgrade-nowait"/>
<xs:enumeration value="upgrade-skiplocked"/>
<xs:enumeration value="write"/>
</xs:restriction>
</xs:simpleType>

View File

@ -1098,10 +1098,13 @@ public class ParentChildTest extends LegacyTestCase {
s3.setCount(3);
Simple s4 = new Simple( Long.valueOf(4) );
s4.setCount(4);
Simple s5 = new Simple( Long.valueOf(5) );
s5.setCount(5);
s.save( s1 );
s.save( s2 );
s.save( s3 );
s.save( s4 );
s.save( s5 );
assertTrue( s.getCurrentLockMode(s1)==LockMode.WRITE );
tx.commit();
s.close();
@ -1116,6 +1119,8 @@ public class ParentChildTest extends LegacyTestCase {
assertTrue( s.getCurrentLockMode(s3)==LockMode.UPGRADE );
s4 = (Simple) s.get(Simple.class, new Long(4), LockMode.UPGRADE_NOWAIT);
assertTrue( s.getCurrentLockMode(s4)==LockMode.UPGRADE_NOWAIT );
s5 = (Simple) s.get(Simple.class, new Long(5), LockMode.UPGRADE_SKIPLOCKED);
assertTrue( s.getCurrentLockMode(s5)==LockMode.UPGRADE_SKIPLOCKED );
s1 = (Simple) s.load(Simple.class, new Long(1), LockMode.UPGRADE); //upgrade
assertTrue( s.getCurrentLockMode(s1)==LockMode.UPGRADE );
@ -1125,6 +1130,8 @@ public class ParentChildTest extends LegacyTestCase {
assertTrue( s.getCurrentLockMode(s3)==LockMode.UPGRADE );
s4 = (Simple) s.load(Simple.class, new Long(4), LockMode.UPGRADE);
assertTrue( s.getCurrentLockMode(s4)==LockMode.UPGRADE_NOWAIT );
s5 = (Simple) s.load(Simple.class, new Long(5), LockMode.UPGRADE);
assertTrue( s.getCurrentLockMode(s5)==LockMode.UPGRADE_SKIPLOCKED );
s.lock(s2, LockMode.UPGRADE); //upgrade
assertTrue( s.getCurrentLockMode(s2)==LockMode.UPGRADE );
@ -1132,7 +1139,9 @@ public class ParentChildTest extends LegacyTestCase {
assertTrue( s.getCurrentLockMode(s3)==LockMode.UPGRADE );
s.lock(s1, LockMode.UPGRADE_NOWAIT);
s.lock(s4, LockMode.NONE);
s.lock(s5, LockMode.UPGRADE_SKIPLOCKED);
assertTrue( s.getCurrentLockMode(s4)==LockMode.UPGRADE_NOWAIT );
assertTrue( s.getCurrentLockMode(s5)==LockMode.UPGRADE_SKIPLOCKED );
tx.commit();
tx = s.beginTransaction();
@ -1141,6 +1150,7 @@ public class ParentChildTest extends LegacyTestCase {
assertTrue( s.getCurrentLockMode(s1)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s2)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s4)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s5)==LockMode.NONE );
s.lock(s1, LockMode.READ); //upgrade
assertTrue( s.getCurrentLockMode(s1)==LockMode.READ );
@ -1162,8 +1172,9 @@ public class ParentChildTest extends LegacyTestCase {
assertTrue( s.getCurrentLockMode(s1)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s2)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s4)==LockMode.NONE );
assertTrue( s.getCurrentLockMode(s5)==LockMode.NONE );
s.delete(s1); s.delete(s2); s.delete(s3); s.delete(s4);
s.delete(s1); s.delete(s2); s.delete(s3); s.delete(s4); s.delete(s5);
tx.commit();
s.close();
}

View File

@ -285,7 +285,10 @@ public abstract class AbstractEntityManagerImpl implements HibernateEntityManage
throw new PersistenceException( "Unable to parse " + AvailableSettings.LOCK_TIMEOUT + ": " + lockTimeout );
}
if ( timeoutSet ) {
if ( timeout < 0 ) {
if ( timeout == LockOptions.SKIP_LOCKED ) {
options.setTimeOut( LockOptions.SKIP_LOCKED );
}
else if ( timeout < 0 ) {
options.setTimeOut( LockOptions.WAIT_FOREVER );
}
else if ( timeout == 0 ) {