mklaca commented on pull request #76:
URL: https://github.com/apache/qpid-broker-j/pull/76#issuecomment-865146813


   Hi @alex-rufous ,
   
   your suggested implementation of the requestConnectionSlot suffers from 
multiple issues, it is difficult to fix them all.
   The simple iteration over VirtualHost and Broker ConnectionLimitProviders 
does not do the same job as VirtualHostConnectionLimiter.
   
   ```
   @Override
   public ConnectionSlot requestConnectionSlot(AMQPConnection<?> connection)
   {
       final Collection<VirtualHostConnectionLimitProvider> hostLimiters = 
_virtualHost.getChildren(VirtualHostConnectionLimitProvider.class);
       final Collection<BrokerConnectionLimitProvider> brokerLimiters = 
_broker.getChildren(BrokerConnectionLimitProvider.class);
       final List<? extends ConnectionLimitProvider> allLimiters = new 
ArrayList<>()
       allLimiters.addAll(hostLimiters);
       allLimiters.addAll(brokerLimiters);
       
       boolean success = false;
       try
       {
           for (ConnectionLimitProvider limiter : allLimiters)
           {
               limiter.registerAndCheckLimit(connection);
           }
           success = true;
       }
       finally
       {
           if (success)
           {
               connection.addDeletedTask(()-> {
                   final Collection<VirtualHostConnectionLimitProvider> 
hostLimiters = 
_virtualHost.getChildren(VirtualHostConnectionLimitProvider.class);
                   final Collection<BrokerConnectionLimitProvider> 
brokerLimiters = _broker.getChildren(BrokerConnectionLimitProvider.class);
                   final List<? extends ConnectionLimitProvider> allLimiters = 
new ArrayList<>()
                   allLimiters.addAll(hostLimiters);
                   allLimiters.addAll(brokerLimiters);
                   for (ConnectionLimitProvider limiter : limiters)
                   {
                       limiter.deregister(connection);
                   }
               })
           }
           else
           {
               for (ConnectionLimitProvider limiter : allLimiters)
               {
                   limiter.deregister(connection);
               }
           }
       }
   }
   ```
   
   I have to notice that 'decrement' and 'revert' are not  same operations in 
case of frequency limit. You need a special 'revert' method to handle 
exceptions properly.
   
   ### Issues:
   
   - **The issues connected with a configuration change:**
   What does happen in following scenario?
   1. 'requestConnectionSlot' collects all limiters.
   2. The connection is registered in all collected limiters, it executes 
register method on every limiter.
   3. The user changes the configuration of the broker.
   4. The delete task collects all limiters.
   5. The connection is de-registered in all collected limiters.
   
   **Examples:**
   
   - There are two limiters (LimiterA, LimiterB) and then the user changes the 
configuration, he adds a new LimiterC. The connection was register only in 
LimiterA and LimiterB and so it has to be deregistered only in LimiterA and 
LimiterB. Your proposed solution will also de-register the connection in the 
LimiterC and that is a bug.
   - There is just single limiter but the user decides to reset the counters, 
the old counter inside the limiter is dropped and a new one is created. 
'requestConnectionSlot' increments the old counter but the new incorrect 
limiter is decremented in the delete task.
   
   The connection could remember the limiters where it was registered but that 
is memory consuming. If the user deleted a limiter then the limiter would 
remain stored inside the connection.
   
   - **The consistency issue:**
   Suppose three limiters LimiterA, LimiterB, LimiterC and the following 
scenario:
   1. The method 'requestConnectionSlot' collects all limiters.
   2. The connection is registered in LimiterA.
   3. The registration of the connection in LimiterB fails and an exception is 
thrown.
   4. The connection is de-registered in LimiterA, LimiterB, LimiterC. That is 
a bug, connection was registered only in LimiterA and so the change has to be 
reverted only in LimiterA.
   
   You need a special 'revert' method that properly reverts the changes and 
checks that the connection was registered in the limiter indeed.
   
   - **The concurrency issues:**
   Every connection should be register in all limiters or it should not be 
registered in any limiter. But the suggested implementation allows a lot of 
intermediate states when the connection is 'half' registered or 'half' 
de-registered. Let have a limiter with frequency limit: one connection per 
minute.
   Suppose following scenario:
   1. A new connection is open at 7:01:00 for user M. It is the first 
connection and so it should pass.
   2. A new connection is open at 7:01:59 for user M. It should be rejected.
   3. A new connection is open at 7:02:01 for user M. It should pass. But there 
could be multiple limiters and the result depends on how fast can the second 
connection revert its changes.
   
   A shared common lock is required that ensures the atomicity of the 
manipulation with counters. The lock has to ensure that the third connection is 
waiting till the second finishes its work.
   
   The simple iteration is not a feasible solution. If we fixed all mentioned 
issues then the solution would become complicated and cluttered and the limiter 
would need a new methods "deregister" and "revert". The solution requires also 
significant changes in the exiting code of AMQPConnection_1_0 and other classes.
   
   **My solution:**
   The 'append' method creates a new chained limiter without any impact on the 
input limiters. It does not have any side effect and so the 'append' operation 
can be repeated.
   `ConnectionLimiter chainedLimiter = limiterA.append(limiterB). // The 
operation does not change neither limiterA nor limiterB. The chainedLimiter is 
a new limiter that contains limiterA and limiterB.`
   The chained limiter ensures proper locking and avoids the concurrency 
issues. Every user has his own independent lock. Hence, multiple connections 
from different users can be processed in parallel. The append operation does 
not have any impact on input limiters. Therefore, it is possible to build a new 
chained limiter while the old limiter stays untouched and in use. It means easy 
update when the configuration is changed.
   
   The chained limiter takes care of the consistency issue. The registration is 
splitted into three steps:
   1. The user's lock is requested at first.
   2. All counters are check.
   3. If only all checks pass then all counters will be incremented.
   
   There is no need for a revert method. The register method returns a 
connection slot, the connection slot points to a counter object. It remembers 
which counter should be decremented when the connection slot is released.
   The connection slots can be chained and so the chained limiter returns a 
chained connection slot. The connection slot remembers correct counters 
regardless of the subsequent configuration change. Therefore, it effectively 
solves problems caused by any configuration change and neither 'deregister' nor 
'revert' method of the limiter are needed.
   
   The cached limiter returns the same connection slot when the same connection 
is registered multiple times, AMQPConnection_1_0 can try to register itself 
multiple times. Cached limiter associates the connection with a slot and the 
following condition is held:
   `cachedLimiter.register(connection) == cachedLimiter.register(connection)`
   The connection slot produced with cached limiter is released only once 
regardless how many times the method 'free' is executed.
   
   All user limiting logic is focused in implementations of ConnectionLimiter 
interface and limiters take care of synchronization, consistency, configuration 
changes.
   
   **BlockEveryone limiter:**
   Let wrap 'BlockEveryone' limiter in cache decorator that utilizes a map as 
the internal cache. But the cache remains empty forever because the register 
method of 'BlockEveryone' limiter always throws an exception. There is not any 
difference in acting of wrapped and unwrapped 'BlockEveryone' limiter.
   'BlockEveryone' limiter acts as cached limiter, hence it is a cached limiter.
   
   **NoLimits limiter:**
   Let wrap 'NoLimits' limiter in cache decorator that utilizes a map as the 
internal cache. But the cache will contain the same response FreeSlot.INSTANCE 
for every input and so it is useless. The wrapped and unwrapped 'NoLimits' 
limiter returns the same response for every input.
   Hence, 'NoLimits' limiter acts as cached limiter and so it is a cached 
limiter.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@qpid.apache.org
For additional commands, e-mail: dev-h...@qpid.apache.org

Reply via email to