001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.service;
016    
017    import java.io.IOException;
018    import java.text.MessageFormat;
019    import java.util.Properties;
020    
021    import javax.persistence.EntityManager;
022    import javax.persistence.EntityManagerFactory;
023    import javax.persistence.Persistence;
024    
025    import org.apache.hadoop.conf.Configuration;
026    import org.apache.oozie.BundleActionBean;
027    import org.apache.oozie.BundleJobBean;
028    import org.apache.oozie.CoordinatorActionBean;
029    import org.apache.oozie.CoordinatorJobBean;
030    import org.apache.oozie.ErrorCode;
031    import org.apache.oozie.FaultInjection;
032    import org.apache.oozie.SLAEventBean;
033    import org.apache.oozie.WorkflowActionBean;
034    import org.apache.oozie.WorkflowJobBean;
035    import org.apache.oozie.client.rest.JsonBundleJob;
036    import org.apache.oozie.client.rest.JsonCoordinatorAction;
037    import org.apache.oozie.client.rest.JsonCoordinatorJob;
038    import org.apache.oozie.client.rest.JsonSLAEvent;
039    import org.apache.oozie.client.rest.JsonWorkflowAction;
040    import org.apache.oozie.client.rest.JsonWorkflowJob;
041    import org.apache.oozie.executor.jpa.JPAExecutor;
042    import org.apache.oozie.executor.jpa.JPAExecutorException;
043    import org.apache.oozie.util.IOUtils;
044    import org.apache.oozie.util.Instrumentable;
045    import org.apache.oozie.util.Instrumentation;
046    import org.apache.oozie.util.XLog;
047    import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
048    
049    /**
050     * Service that manages JPA and executes {@link JPAExecutor}.
051     */
052    public class JPAService implements Service, Instrumentable {
053        private static final String INSTRUMENTATION_GROUP = "jpa";
054    
055        public static final String CONF_DB_SCHEMA = "oozie.db.schema.name";
056    
057        public static final String CONF_PREFIX = Service.CONF_PREFIX + "JPAService.";
058        public static final String CONF_URL = CONF_PREFIX + "jdbc.url";
059        public static final String CONF_DRIVER = CONF_PREFIX + "jdbc.driver";
060        public static final String CONF_USERNAME = CONF_PREFIX + "jdbc.username";
061        public static final String CONF_PASSWORD = CONF_PREFIX + "jdbc.password";
062        public static final String CONF_MAX_ACTIVE_CONN = CONF_PREFIX + "pool.max.active.conn";
063        public static final String CONF_CREATE_DB_SCHEMA = CONF_PREFIX + "create.db.schema";
064        public static final String CONF_VALIDATE_DB_CONN = CONF_PREFIX + "validate.db.connection";
065    
066        private EntityManagerFactory factory;
067        private Instrumentation instr;
068    
069        private static XLog LOG;
070    
071        /**
072         * Return the public interface of the service.
073         *
074         * @return {@link JPAService}.
075         */
076        public Class<? extends Service> getInterface() {
077            return JPAService.class;
078        }
079    
080        @Override
081        public void instrument(Instrumentation instr) {
082            this.instr = instr;
083        }
084    
085        /**
086         * Initializes the {@link JPAService}.
087         *
088         * @param services services instance.
089         */
090        public void init(Services services) throws ServiceException {
091            LOG = XLog.getLog(JPAService.class);
092            Configuration conf = services.getConf();
093            String dbSchema = conf.get(CONF_DB_SCHEMA, "oozie");
094            String url = conf.get(CONF_URL, "jdbc:derby:${oozie.home.dir}/${oozie.db.schema.name}-db;create=true");
095            String driver = conf.get(CONF_DRIVER, "org.apache.derby.jdbc.EmbeddedDriver");
096            String user = conf.get(CONF_USERNAME, "sa");
097            String password = conf.get(CONF_PASSWORD, "").trim();
098            String maxConn = conf.get(CONF_MAX_ACTIVE_CONN, "10").trim();
099            boolean autoSchemaCreation = conf.getBoolean(CONF_CREATE_DB_SCHEMA, true);
100            boolean validateDbConn = conf.getBoolean(CONF_VALIDATE_DB_CONN, false);
101    
102            if (!url.startsWith("jdbc:")) {
103                throw new ServiceException(ErrorCode.E0608, url, "invalid JDBC URL, must start with 'jdbc:'");
104            }
105            String dbType = url.substring("jdbc:".length());
106            if (dbType.indexOf(":") <= 0) {
107                throw new ServiceException(ErrorCode.E0608, url, "invalid JDBC URL, missing vendor 'jdbc:[VENDOR]:...'");
108            }
109            dbType = dbType.substring(0, dbType.indexOf(":"));
110    
111            String persistentUnit = "oozie-" + dbType;
112    
113            // Checking existince of ORM file for DB type
114            String ormFile = "META-INF/" + persistentUnit + "-orm.xml";
115            try {
116                IOUtils.getResourceAsStream(ormFile, -1);
117            }
118            catch (IOException ex) {
119                throw new ServiceException(ErrorCode.E0609, dbType, ormFile);
120            }
121    
122            String connProps = "DriverClassName={0},Url={1},Username={2},Password={3},MaxActive={4}";
123            connProps = MessageFormat.format(connProps, driver, url, user, password, maxConn);
124            Properties props = new Properties();
125            if (autoSchemaCreation) {
126                props.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
127            }
128            else if (validateDbConn) {
129                // validation can be done only if the schema already exist, else a
130                // connection cannot be obtained to create the schema.
131                connProps += ",TestOnBorrow=true,TestOnReturn=false,TestWhileIdle=false";
132                connProps += ",ValidationQuery=select count(*) from VALIDATE_CONN";
133                connProps = MessageFormat.format(connProps, dbSchema);
134            }
135            props.setProperty("openjpa.ConnectionProperties", connProps);
136    
137            factory = Persistence.createEntityManagerFactory(persistentUnit, props);
138    
139            EntityManager entityManager = getEntityManager();
140            entityManager.find(WorkflowActionBean.class, 1);
141            entityManager.find(WorkflowJobBean.class, 1);
142            entityManager.find(CoordinatorActionBean.class, 1);
143            entityManager.find(CoordinatorJobBean.class, 1);
144            entityManager.find(JsonWorkflowAction.class, 1);
145            entityManager.find(JsonWorkflowJob.class, 1);
146            entityManager.find(JsonCoordinatorAction.class, 1);
147            entityManager.find(JsonCoordinatorJob.class, 1);
148            entityManager.find(SLAEventBean.class, 1);
149            entityManager.find(JsonSLAEvent.class, 1);
150            entityManager.find(BundleJobBean.class, 1);
151            entityManager.find(JsonBundleJob.class, 1);
152            entityManager.find(BundleActionBean.class, 1);
153    
154            LOG.info(XLog.STD, "All entities initialized");
155            // need to use a pseudo no-op transaction so all entities, datasource
156            // and connection pool are initialized one time only
157            entityManager.getTransaction().begin();
158            OpenJPAEntityManagerFactorySPI spi = (OpenJPAEntityManagerFactorySPI) factory;
159            LOG.info("JPA configuration: {0}", spi.getConfiguration().getConnectionProperties());
160            entityManager.getTransaction().commit();
161            entityManager.close();
162        }
163    
164        /**
165         * Destroy the JPAService
166         */
167        public void destroy() {
168            if (factory != null && factory.isOpen()) {
169                factory.close();
170            }
171        }
172    
173        /**
174         * Execute a {@link JPAExecutor}.
175         *
176         * @param executor JPAExecutor to execute.
177         * @return return value of the JPAExecutor.
178         * @throws JPAExecutorException thrown if an jpa executor failed
179         */
180        public <T> T execute(JPAExecutor<T> executor) throws JPAExecutorException {
181            EntityManager em = getEntityManager();
182            Instrumentation.Cron cron = new Instrumentation.Cron();
183            try {
184                LOG.trace("Executing JPAExecutor [{0}]", executor.getName());
185                if (instr != null) {
186                    instr.incr(INSTRUMENTATION_GROUP, executor.getName(), 1);
187                }
188                cron.start();
189                em.getTransaction().begin();
190                T t = executor.execute(em);
191                if (em.getTransaction().isActive()) {
192                    if (FaultInjection.isActive("org.apache.oozie.command.SkipCommitFaultInjection")) {
193                        throw new RuntimeException("Skipping Commit for Failover Testing");
194                    }
195    
196                    em.getTransaction().commit();
197                }
198                return t;
199            }
200            finally {
201                cron.stop();
202                if (instr != null) {
203                    instr.addCron(INSTRUMENTATION_GROUP, executor.getName(), cron);
204                }
205                try {
206                    if (em.getTransaction().isActive()) {
207                        LOG.warn("JPAExecutor [{0}] ended with an active transaction, rolling back", executor.getName());
208                        em.getTransaction().rollback();
209                    }
210                }
211                catch (Exception ex) {
212                    LOG.warn("Could not check/rollback transaction after JPAExecutor [{0}], {1}", executor.getName(), ex
213                            .getMessage(), ex);
214                }
215                try {
216                    if (em.isOpen()) {
217                        em.close();
218                    }
219                    else {
220                        LOG.warn("JPAExecutor [{0}] closed the EntityManager, it should not!", executor.getName());
221                    }
222                }
223                catch (Exception ex) {
224                    LOG.warn("Could not close EntityManager after JPAExecutor [{0}], {1}", executor.getName(), ex
225                            .getMessage(), ex);
226                }
227            }
228        }
229    
230        /**
231         * Return an EntityManager. Used by the StoreService. Once the StoreService is removed this method must be removed.
232         *
233         * @return an entity manager
234         */
235        EntityManager getEntityManager() {
236            return factory.createEntityManager();
237        }
238    
239    }