/*
 * ConfigIncrement.actor.cpp
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
 *
 * Licensed 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.
 */

#include "fdbclient/ISingleThreadTransaction.h"
#include "fdbclient/Tuple.h"
#include "fdbserver/workloads/workloads.actor.h"
#include "flow/actorcompiler.h" // This must be the last #include.

class ConfigIncrementWorkload : public TestWorkload {
	int incrementActors{ 0 };
	int incrementsPerActor{ 0 };
	Version lastKnownCommittedVersion{ ::invalidVersion };
	int lastKnownValue{ -1 };
	double meanSleepWithinTransactions{ 0.01 };
	double meanSleepBetweenTransactions{ 0.1 };

	static KeyRef const testKnobName;
	static Key configKey;

	PerfIntCounter transactions, retries, commitUnknownResult;

	static Key getConfigKey() { return Tuple::makeTuple(/* config class */ nullptr, testKnobName).pack(); }

	ACTOR static Future<int> get(Reference<ISingleThreadTransaction> tr) {
		state TraceEvent te(SevDebug, "ConfigIncrementGet");
		Optional<Value> serializedValue = wait(tr->get(getConfigKey()));
		if (!serializedValue.present()) {
			return 0;
		} else {
			Tuple t = Tuple::unpack(serializedValue.get());
			int value = t.getInt(0);
			te.detail("Value", value);
			return value;
		}
	}

	static void set(Reference<ISingleThreadTransaction> tr, int value) {
		TraceEvent(SevDebug, "ConfigIncrementSet").detail("Value", value);
		tr->set(getConfigKey(), format("%d", value));
	}

	ACTOR static Future<Void> incrementActor(ConfigIncrementWorkload* self, Database cx) {
		state int trsComplete = 0;
		state Reference<ISingleThreadTransaction> tr;
		TraceEvent(SevDebug, "ConfigIncrementStartIncrementActor");
		while (trsComplete < self->incrementsPerActor) {
			try {
				loop {
					try {
						tr = self->getTransaction(cx);
						state int currentValue = wait(get(tr));
						ASSERT_GE(currentValue, self->lastKnownValue);
						set(tr, currentValue + 1);
						wait(delay(deterministicRandom()->random01() * 2 * self->meanSleepWithinTransactions));
						wait(tr->commit());
						ASSERT_GT(tr->getCommittedVersion(), self->lastKnownCommittedVersion);
						self->lastKnownCommittedVersion = tr->getCommittedVersion();
						self->lastKnownValue = currentValue + 1;
						TraceEvent("ConfigIncrementSucceeded")
						    .detail("CommittedVersion", self->lastKnownCommittedVersion)
						    .detail("CommittedValue", self->lastKnownValue);
						++self->transactions;
						++trsComplete;
						wait(delay(deterministicRandom()->random01() * 2 * self->meanSleepBetweenTransactions));
						break;
					} catch (Error& e) {
						TraceEvent(SevDebug, "ConfigIncrementError")
						    .errorUnsuppressed(e)
						    .detail("LastKnownValue", self->lastKnownValue);
						wait(tr->onError(e));
						++self->retries;
					}
				}
			} catch (Error& e) {
				if (e.code() == error_code_commit_unknown_result) {
					++self->commitUnknownResult;
					wait(delayJittered(0.1));
					tr->reset();
				} else {
					throw e;
				}
			}
		}
		return Void();
	}

	ACTOR static Future<bool> check(ConfigIncrementWorkload* self, Database cx) {
		state Reference<ISingleThreadTransaction> tr;
		loop {
			tr.clear();
			try {
				tr = self->getTransaction(cx);
				state int currentValue = wait(get(tr));
				auto expectedValue = self->incrementActors * self->incrementsPerActor;
				TraceEvent("ConfigIncrementCheck")
				    .detail("CurrentValue", currentValue)
				    .detail("ExpectedValue", expectedValue);
				return currentValue >= expectedValue; // >= because we may have maybe_committed errors
			} catch (Error& e) {
				wait(tr->onError(e));
			}
		}
	}

	Reference<ISingleThreadTransaction> getTransaction(Database cx) const {
		ASSERT(g_network->isSimulated()); // TODO: Enforce elsewhere
		ASSERT(g_simulator->configDBType != ConfigDBType::DISABLED);
		auto type = (g_simulator->configDBType == ConfigDBType::SIMPLE) ? ISingleThreadTransaction::Type::SIMPLE_CONFIG
		                                                                : ISingleThreadTransaction::Type::PAXOS_CONFIG;
		return ISingleThreadTransaction::create(type, cx);
	}

public:
	static constexpr auto NAME = "ConfigIncrement";
	ConfigIncrementWorkload(WorkloadContext const& wcx)
	  : TestWorkload(wcx), transactions("Transactions"), retries("Retries"),
	    commitUnknownResult("CommitUnknownResult") {
		incrementActors = getOption(options, "incrementActors"_sr, 10);
		incrementsPerActor = getOption(options, "incrementsPerActor"_sr, 10);
		meanSleepWithinTransactions = getOption(options, "meanSleepWithinTransactions"_sr, 0.01);
		meanSleepBetweenTransactions = getOption(options, "meanSleepBetweenTransactions"_sr, 0.1);
	}

	Future<Void> setup(Database const& cx) override { return Void(); }

	Future<Void> start(Database const& cx) override {
		std::vector<Future<Void>> actors;
		auto localIncrementActors =
		    (clientId < incrementActors) ? ((incrementActors - clientId - 1) / clientCount + 1) : 0;
		for (int i = 0; i < localIncrementActors; ++i) {
			actors.push_back(incrementActor(this, cx));
		}
		return waitForAll(actors);
	}

	Future<bool> check(Database const& cx) override { return clientId ? Future<bool>{ true } : check(this, cx); }

	void getMetrics(std::vector<PerfMetric>& m) override {
		m.push_back(transactions.getMetric());
		m.push_back(retries.getMetric());
		m.push_back(commitUnknownResult.getMetric());
		m.emplace_back("Last Known Value", lastKnownValue, Averaged::False);
	}
};

WorkloadFactory<ConfigIncrementWorkload> ConfigIncrementWorkloadFactory;

KeyRef const ConfigIncrementWorkload::testKnobName = "test_int"_sr;
