content: /*
   * 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.cassandra.distributed.upgrade;
  
  import java.util.Arrays;
  import java.util.List;
  import java.util.Set;
  
  import org.junit.Test;
  
  import org.agrona.collections.IntHashSet;
  import org.apache.cassandra.distributed.api.Feature;
  import org.apache.cassandra.distributed.api.ICoordinator;
  import org.assertj.core.api.Assertions;
  
  import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
  import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
  import static org.apache.cassandra.distributed.shared.AssertUtils.row;
  
  /**
   * Tests the CQL functions {@code writetime}, {@code maxwritetime} and {@code ttl} on rolling upgrade.
   *
   * {@code writetime} and {@code ttl} on single-cell columns is always supported, even in mixed clusters.
   * {@code writetime} and {@code ttl} on multi-cell columns is not supported in coordinator nodes < 4.2.
   * {@code maxwritetime} is not supported in coordinator nodes < 4.2.
   */
  public class MixedModeWritetimeOrTTLTest extends UpgradeTestBase
  {
      private static final String CANNOT_USE_SELECTION_FUNCTION_WRITE_TIME_ON_NON_FROZEN_COLLECTION_S = "Cannot use selection function writeTime on non-frozen collection s";
      private static final String CANNOT_USE_SELECTION_FUNCTION_TTL_ON_NON_FROZEN_COLLECTION_S = "Cannot use selection function ttl on non-frozen collection s";
      private static final String MAXWRITETIME_UNKNOWN = "Unknown function 'maxwritetime'";
  
      @Test
      public void testWritetimeOrTTLDuringUpgrade() throws Throwable
      {
          AllowedErrors allowedErrors = new AllowedErrors();
          new TestCase()
          .nodes(2)
          .nodesToUpgradeOrdered(1, 2)
          // all upgrades from v40 to current, excluding v50 -> v51
          .singleUpgradeToCurrentFrom(v40)
          .singleUpgradeToCurrentFrom(v41)
          .withConfig(c -> c.with(Feature.GOSSIP))
          .setup(cluster -> {
              allowedErrors.clear();
              cluster.setUncaughtExceptionsFilter(allowedErrors::uncaughtExceptionsFilter);
  
              ICoordinator coordinator = cluster.coordinator(1);
              cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int PRIMARY KEY, v int, s set<int>, fs frozen<set<int>>)"));
              coordinator.execute(withKeyspace("INSERT INTO %s.t (k, v, s, fs) VALUES (0, 0, {0, 1}, {0, 1, 2, 3}) USING TIMESTAMP 1 AND TTL 1000"), ALL);
              coordinator.execute(withKeyspace("UPDATE %s.t USING TIMESTAMP 2 AND TTL 2000 SET v = 1, s = s + {2, 3} WHERE k = 0"), ALL);
  
              assertPre42Behaviour(cluster.coordinator(1));
              assertPre42Behaviour(cluster.coordinator(2));
          })
          .runAfterNodeUpgrade((cluster, node) -> {
              allowedErrors.upgraded(node);
              if (node == 1) // only node1 is upgraded, and the cluster is in mixed mode
              {
                  assertPost42Behaviour(cluster.coordinator(1));
                  assertPre42Behaviour(cluster.coordinator(2));
              }
              else // both nodes have been upgraded, and the cluster isn't in mixed mode anymore
              {
                  assertPost42Behaviour(cluster.coordinator(1));
                  assertPost42Behaviour(cluster.coordinator(2));
              }
          })
          .run();
      }
  
      private void assertPre42Behaviour(ICoordinator coordinator)
      {
          // regular column, supported except for maxwritetime
          assertRows(coordinator.execute(withKeyspace("SELECT writetime(v) FROM %s.t"), ALL), row(2L));
          Assertions.assertThatThrownBy(() -> coordinator.execute(withKeyspace("SELECT maxwritetime(v) FROM %s.t"), ALL))
                    .hasMessageContaining(MAXWRITETIME_UNKNOWN);
          Assertions.assertThat((Integer) coordinator.execute(withKeyspace("SELECT ttl(v) FROM %s.t"), ALL)[0][0])
                    .isLessThanOrEqualTo(2000).isGreaterThan(2000 - 300); // margin of error of 5 minutes since TTLs decrease
  
          // frozen collection, supported except for maxwritetime
          assertRows(coordinator.execute(withKeyspace("SELECT writetime(fs) FROM %s.t"), ALL), row(1L));
          Assertions.assertThatThrownBy(() -> coordinator.execute(withKeyspace("SELECT maxwritetime(fs) FROM %s.t"), ALL))
                    .hasMessageContaining(MAXWRITETIME_UNKNOWN);
          Assertions.assertThat((Integer) coordinator.execute(withKeyspace("SELECT ttl(fs) FROM %s.t"), ALL)[0][0])
                    .isLessThanOrEqualTo(1000).isGreaterThan(1000 - 300); // margin of error of 5 minutes since TTLs decrease
  
          // not-frozen collection, not supported
          Assertions.assertThatThrownBy(() -> coordinator.execute(withKeyspace("SELECT writetime(s) FROM %s.t"), ALL))
                    .hasMessageContaining(CANNOT_USE_SELECTION_FUNCTION_WRITE_TIME_ON_NON_FROZEN_COLLECTION_S);
          Assertions.assertThatThrownBy(() -> coordinator.execute(withKeyspace("SELECT maxwritetime(s) FROM %s.t"), ALL))
                    .hasMessageContaining(MAXWRITETIME_UNKNOWN);
          Assertions.assertThatThrownBy(() -> coordinator.execute(withKeyspace("SELECT ttl(s) FROM %s.t"), ALL))
                    .hasMessageContaining(CANNOT_USE_SELECTION_FUNCTION_TTL_ON_NON_FROZEN_COLLECTION_S);
      }
  
      private void assertPost42Behaviour(ICoordinator coordinator)
      {
          // regular column, fully supported
          assertRows(coordinator.execute(withKeyspace("SELECT writetime(v) FROM %s.t"), ALL), row(2L));
          assertRows(coordinator.execute(withKeyspace("SELECT maxwritetime(v) FROM %s.t"), ALL), row(2L));
          Assertions.assertThat((Integer) coordinator.execute(withKeyspace("SELECT ttl(v) FROM %s.t"), ALL)[0][0])
                    .isLessThanOrEqualTo(2000).isGreaterThan(2000 - 300); // margin of error of 5 minutes since TTLs decrease
  
          // frozen collection, fully supported
          assertRows(coordinator.execute(withKeyspace("SELECT writetime(fs) FROM %s.t"), ALL), row(1L));
          assertRows(coordinator.execute(withKeyspace("SELECT maxwritetime(fs) FROM %s.t"), ALL), row(1L));
          Assertions.assertThat((Integer) coordinator.execute(withKeyspace("SELECT ttl(fs) FROM %s.t"), ALL)[0][0])
                    .isLessThanOrEqualTo(1000).isGreaterThan(1000 - 300); // margin of error of 5 minutes since TTLs decrease
  
          // not-frozen collection, fully supported
          assertRows(coordinator.execute(withKeyspace("SELECT writetime(s) FROM %s.t"), ALL), row(Arrays.asList(1L, 1L, 2L, 2L)));
          assertRows(coordinator.execute(withKeyspace("SELECT maxwritetime(s) FROM %s.t"), ALL), row(2L));
          Assertions.assertThat(coordinator.execute(withKeyspace("SELECT ttl(s) FROM %s.t"), ALL)[0][0])
                    .matches(l -> l instanceof List && ((List<?>) l).size() == 4);
      }
  
      private static class AllowedErrors
      {
          private static final Set<String> EXPECTED_ERRORS = Set.of(MAXWRITETIME_UNKNOWN,
                                                                    CANNOT_USE_SELECTION_FUNCTION_WRITE_TIME_ON_NON_FROZEN_COLLECTION_S,
                                                                    CANNOT_USE_SELECTION_FUNCTION_TTL_ON_NON_FROZEN_COLLECTION_S);
  
          private final IntHashSet upgraded = new IntHashSet();
  
          private void clear()
          {
              upgraded.clear();
          }
  
          private void upgraded(int node)
          {
              upgraded.add(node);
          }
  
          private boolean uncaughtExceptionsFilter(int node, Throwable t)
          {
              String message = t.getMessage();
              if (message != null && EXPECTED_ERRORS.contains(message))
              {
                  // upgraded nodes should not produce these errors
                  return !upgraded.contains(node);
              }
              return false;
          }
      }
  }
  