CloudStack has its own database framework that is based on DAO.
CloudStack uses MySQL as the database, any schema for any feature maybe define
in SQL. Before you start understanding and defining the DAO framework and its
usage in CloudStack, understand how CloudStack schema is defined and upgraded.
Use of terminal based mysql-client
is recommended, you may also use MySQL
workbench.
References:
- https://www.ibm.com/developerworks/java/library/j-genericdao/index.html
- https://www.w3schools.com/sql/
- https://www.tutorialspoint.com/sql/
- http://www.mysqltutorial.org/
- https://cwiki.apache.org/confluence/display/CLOUDSTACK/DB+Upgrade+in+CloudStack
CloudStack has following databases:
cloud
: the main database where most of CloudStack tables arecloud_usage
: the database used by the usage server for usage record generationsimulator
: the database used by simulator plugin (only for developers, not for production usage)
The DatabaseUpgradeChecker
class is responsible for CloudStack schema upgrade.
When the management server starts, an instance of this class kicks in to
check the version of the schema based on the cloud.version
table against the
code version (from the jar). This class defines a map/sequence/hierarchy of
upgrade paths from a starting version number. The upgrade path is a class
that implements the DbUpgrade
interface. For example, if the code version
(as defined in the root pom.xml file) is 4.12.0.0-SNAPSHOT
look for an upgrade
path class that may be named as Upgrade4XXXXto41200
. This class would define
two sql scripts, one that upgrades CloudStack's schema and other that runs
to perform any schema cleanup. This class also defines metadata about the
upgrade path, the from/to version ranges etc. You may use any of the existing
upgrade paths to learn how to write one as well.
Pick the upgrade path for which you intend your feature for (i.e. the target
CloudStack version), and add relevant schema to that sql upgrade path file
(for example, META-INF/db/schema-41120to41200.sql
). For example:
CREATE TABLE IF NOT EXISTS `cloud`.`coffee` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) UNIQUE,
`name` varchar(255) NOT NULL,
`state` varchar(40) NOT NULL,
`account_id` bigint unsigned NOT NULL,
`created` datetime NOT NULL COMMENT 'date of creation',
`removed` datetime COMMENT 'date of removal',
PRIMARY KEY (`id`),
KEY (`uuid`),
KEY `i_coffee` (`name`, `account_id`, `created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Tips:
- Avoid using plural form for the table name (for example,
cloud.user
notcloud.users
) - Don't create schema keys that you won't need
- For performance gains use
views
instead of simply thetables
, the general use case could be to speed up list API execution
Recommended reading: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Data+Access+Layer
CloudStack data access layer is implemented by GenericDaoBase
abstract class
that implements the DAO. For feature/resource specific tables, you would usually
implement a Dao
interface that defines the contract for the DaoImpl
, the
implementation class would extend GenericDaoBase
.
The VO (view object) captures the schema/table and a VO instance typically represents a row in the table. The VO typically implements the resource interface (contract), however for purpose of any subsystem consuming a resource object, the resource interface should be used/passed around than a VO instance.
The VO class exports and use several annotations for its table/db fields and
@Table
to define the name of the table that the VO represents. Most CloudStack
tables have an internal (bigint) ID or database id
, but the resources are
queries by users based on an external string based uuid
.
Define the VO
and make it implement the feature/resource interface. For
example:
@Entity
@Table(name = "coffee")
public class CoffeeVO implements Coffee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "uuid")
private String uuid;
@Column(name = "name")
private String name;
@Column(name = "state", nullable = false)
@Enumerated(value = EnumType.STRING)
private CoffeeState state = CoffeeState.Created;
@Column(name = "account_id")
private long accountId;
@Column(name = GenericDao.CREATED_COLUMN)
protected Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
protected Date removed;
// This empty constructor is needed for reflection to work
public CoffeeVO() {
uuid = UUID.randomUUID().toString();
}
// Custom constructor example
public CoffeeVO(String name, long accountId) {
this.uuid = UUID.randomUUID().toString();
this.name = name;
this.accountId = accountId;
}
// more custom constructors, getters, setters etc.
Define the Dao
interface, for example:
package org.apache.cloudstack.feature.dao;
public interface CoffeeDao extends GenericDao<CoffeeVO, Long> {
// method definitions here
}
Define the DaoImpl
, for example:
package org.apache.cloudstack.feature.dao;
public class CoffeeDaoImpl extends GenericDaoBase<CoffeeVO, Long> implements CoffeeDao {
// method implementations here
}
Declare the DaoImpl
in the spring context xml file so that a bean gets created
and can be injected in the service layer class. For example:
--- a/plugins/hackerbook/feature/src/main/resources/META-INF/cloudstack/feature/spring-feature-context.xml
+++ b/plugins/hackerbook/feature/src/main/resources/META-INF/cloudstack/feature/spring-feature-context.xml
@@ -2,6 +2,9 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="coffeeManager" class="org.apache.cloudstack.feature.CoffeeManagerImpl" />
+ <bean id="coffeeDao" class="org.apache.cloudstack.feature.dao.CoffeeDaoImpl" />
Now, the DaoImpl
class can be injected and used by the service/manager class.
For example:
public class CoffeeManagerImpl extends ManagerBase implements CoffeeManager, Configurable, PluggableService {
// .. code redacted ..
@Inject
private CoffeeDao coffeeDao;
// .. code redacted ..
@Override
public List<Coffee> listCoffees(ListCoffeesCmd cmd) {
// Perform validations checks etc. following is just an example
return new ArrayList<>(coffeeDao.listAll());
}
// .. code redacted ..
@Override
@ActionEvent(eventType = EventTypes.EVENT_COFFEE_CREATE, eventDescription = "creating coffee", async = true)
public Coffee createCoffee(CreateCoffeeCmd cmd) {
// Perform validations checks etc. following is just an example
return coffeeDao.persist(new CoffeeVO(cmd.getName(), CallContext.current().getCallingAccountId()));
}
By default, the Dao will have several ready to use methods such as listAll
,
findById
, update
, persist
, remove
etc. When persisting a new VO, your
code does not need to create/set the id
, created
which are handled by the
DAO framework (GenericDaoBase).
Read the DAO wiki article to know various building blocks and utilities you can
use to create custom methods for searching and querying (for example, the
SearchBuilder
).
The ORM uses interceptors to track which fields of a VO have changed to construct the SQL query. It determines the field / column to be searched or updated from the getter or setter used. Therefore it is important to name the getters and setters properly. eg: A variable "abc" should have the appropriate getter "getAbc()" and setter "setAbc()". It is safer to allow the IDE to create the getters / setters. Be especially careful with boolean variables, which need to follow the same get / set prefix for thier getters and setters rather than is / enable / disable.
Recommended reading: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Database+Transactions
You can wrap a set of complex DB operations (for example, deleting of details
when coffee is deleted/removed) in a DB transaction using the Transaction
utility. For example:
return Transaction.execute(new TransactionCallback<CoffeeVO>() {
@Override
public CoffeeVO doInTransaction(TransactionStatus status) {
return coffeeDao.persist(new CoffeeVO(name, accountId));
}
});
For any resource, a details table can be designed as well. The VOs and Daos
can be implemented in the same way. Typically a _details
table will have
following columns:
id
: the auto increment internal DB ID (bigint)resource_id
: the (bigint) ID of the resourcename
: the name/key of the detail (string)value
: the value of the detail (string)display
: boolean (int:1) to show/hide that detail
The VO
class for the resource detail table can implement ResourceDetail
interface. The Dao
interface can extend the ResourceDetailsDao<R>
interface
and the DaoImpl
class can extend the ResourceDetailsDaoBase<RVO>
abstract
class. Using this pattern, your resource's DetailsDao
can be used to
list/add/remove/save details.
A Finite State Machine or
FSM defines a transition
table that takes in a state and an event to transition to a new state. In
CloudStack FSM is used to implement state machine and restrict how state of a
resource such as a VM, volume, network etc. transition given an event occurs.
The resource state and events are both defined as enum
, usually in the
resource interface. The resource interface need to extend the StateObject<S>
interface. For example:
public interface Coffee extends InternalIdentity, Identity, StateObject<Coffee.CoffeeState> {
// .. code redacted ..
enum Event {
OrderReceived,
OrderReady,
OrderDiscarded
}
enum CoffeeState {
Created, Brewing, Brewed, Discarded;
private final static StateMachine2<CoffeeState, Event, Coffee> FSM = new StateMachine2<>();
static {
FSM.addTransition(Created, Event.OrderReceived, Brewing);
FSM.addTransition(Brewing, Event.OrderReady, Brewed);
FSM.addTransitionFromStates(Event.OrderDiscarded, Discarded, Created, Brewing, Brewed);
}
public static StateMachine2<CoffeeState, Event, Coffee> getStateMachine() {
return FSM;
}
}
// .. code redacted ..
CoffeeState getState();
The resource specific Dao
class needs to handle state transitions, Daos of
resources that are StateObject
can extend/implement StateDao<S, E, V>
.
public interface CoffeeDao extends GenericDao<CoffeeVO, Long>, StateDao<Coffee.CoffeeState, Coffee.Event, Coffee> {
// .. code redacted ..
The DaoImpl
can implement updateState
. For example:
public class CoffeeDaoImpl extends GenericDaoBase<CoffeeVO, Long> implements CoffeeDao {
@Override
public boolean updateState(Coffee.CoffeeState currentState, Coffee.Event event, Coffee.CoffeeState nextState, Coffee vo, Object data) {
// Update logic/check here
// May use an update_counter from the vo for lock-free update
vo.setState(nextState);
return update(vo.getId(), (CoffeeVO) vo);
}
Tip: due to several threads of execution and multiple management server, it may
be possible that for the same resource object (VO instance) its state
may get
updated. For this purpose, in several CloudStack VOs an update_counter
may be
defined that ties up with the updates of its State
. This provides a lock-free
mechanism of updating a resource state, where the logic is simply enforced using
a SearchBuilder
that updates a VO
object based on its previous update
counter value and its previous state. For example, see VolumeVO
and
VolumeDaoImpl
.
With the FSM implemented and DAO made state-aware, the FSM can be then used to
transit to a state based on an event which can automatically handle any DB
updates. This can be done by having a utility method that uses FSM's transitTo
method. For example, the following method in the service/manager impl class:
private boolean stateTransitTo(Coffee coffee, Coffee.Event event) throws NoTransitionException {
return Coffee.CoffeeState.getStateMachine().transitTo(coffee, event, null, coffeeDao);
}
You can also add a listener of state changes to a CloudStack FSM using
registerListener()
, the listener could be any class that implements the
StateListener<S, E, V>
interface.
-
Implement the VO, Dao and DaoImpl classes for your feature, for each of the schema/tables
coffee
andcoffee_details
. -
Integrate DB with the service layer, ensure all the APIs actually perform CRUD against the DB.
-
Write/update the unit tests (tip: use
mockito
to mock db interactions for the manager impl unit test, you can run a unit test using@RunWith(MockitoJUnitRunner.class)
, and use@Spy
and@InjectMocks
on your service/manager impl class). -
Implement FSM, use events for state transition and integrate it with a custom brewing simulation algorithm that introduces random sleep intervals. Finish the feature based on the spec.
Challenge: Attempt and fix any upstream CloudStack DB related issue(s): https://github.com/apache/cloudstack/issues