/*
* Copyright 2013 Eediom Inc.
*
* 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.
*/
package org.araqne.log.api.impl;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Validate;
import org.araqne.api.PrimitiveConverter;
import org.araqne.log.api.LastState;
import org.araqne.log.api.LastStateListener;
import org.araqne.log.api.LastStateService;
import org.json.JSONConverter;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.json.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manage last state of logger. Each logger state is persist as single JSON
* file.
*
* @author xeraph
* @since 2.9.0
*/
@Component(name = "last-state-service")
@Provides
public class LastStateServiceImpl implements LastStateService {
private final Logger slog = LoggerFactory.getLogger(LastStateServiceImpl.class);
/**
* last state JSON directory
*/
private File dir;
/**
* in-memory last state cache
*/
private ConcurrentMap<String, LastState> states = new ConcurrentHashMap<String, LastState>();
private CopyOnWriteArraySet<LastStateListener> listeners = new CopyOnWriteArraySet<LastStateListener>();
/**
* last state file sync in batch mode
*/
private FileSyncThread sync;
/**
* reject last state update while closing service
*/
private volatile boolean reject;
@Validate
public void start() {
reject = false;
ensureRepository();
// load all states
loadAllFiles();
// start sync thread
sync = new FileSyncThread();
sync.start();
}
private void loadAllFiles() {
File[] l = dir.listFiles();
if (l == null)
return;
for (File f : l) {
// file name format is namespace$name.state or
// node$namespace$name.state
if (!f.getName().endsWith(".state") || !f.getName().contains("$"))
continue;
try {
LastState s = readStateFile(f);
states.put(s.getLoggerName(), s);
} catch (IOException e) {
slog.error("araqne log api: cannot load last state file [" + f.getAbsolutePath() + "]", e);
}
}
}
private void ensureRepository() {
String path = System.getProperty("araqne.logapi.state.dir");
if (path != null)
dir = new File(path);
else
dir = new File(System.getProperty("araqne.data.dir"), "araqne-log-api/state");
if (dir.mkdirs())
slog.info("araqne log api: last state repository [{}] is created", dir.getAbsolutePath());
}
@Invalidate
public void stop() {
reject = true;
// wait until file sync is done
try {
sync.doStop = true;
sync.join();
sync = null;
slog.info("araqne log api: state thread ended");
} catch (InterruptedException e) {
slog.warn("araqne log api: last state sync thread join interrupted", e);
}
}
@Override
public List<LastState> getStates() {
return new ArrayList<LastState>(states.values());
}
@Override
public LastState getState(String name) {
LastState old = states.get(name);
if (old == null)
return null;
return LastState.cloneState(old);
}
@Override
public void setState(LastState state) {
if (state == null)
throw new IllegalArgumentException("last state should not be null");
if (reject)
throw new IllegalStateException("cannot update last state of logger [" + state.getLoggerName()
+ "], service is closing");
// skip disk update if state is not changed at all
state = LastState.cloneState(state);
LastState old = states.get(state.getLoggerName());
if (old != null && old.equals(state)) {
slog.debug("araqne log api: logger [{}] same state for update", state.getLoggerName());
return;
}
// update count can be assigned from caller
if (old != null && state.getUpdateCount() == 0)
state.setUpdateCount(old.getUpdateCount() + 1);
states.put(state.getLoggerName(), state);
// queue disk sync
while (true) {
try {
sync.queue.put(state);
break;
} catch (InterruptedException e) {
slog.debug("araqne log api: interrupted last state update of logger [{}]", state.getLoggerName());
}
}
}
@Override
public void deleteState(String loggerName) {
DeleteState state = new DeleteState();
state.setLoggerName(loggerName);
// queue disk sync
try {
sync.queue.put(state);
} catch (InterruptedException e) {
slog.warn("araqne log api: interrupted last state update of logger [{}]", state.getLoggerName());
}
states.remove(loggerName);
}
@Override
public void addListener(LastStateListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(LastStateListener listener) {
listeners.remove(listener);
}
private class DeleteState extends LastState {
}
private class FileSyncThread extends Thread {
private boolean doStop = false;
// fairness is required for update ordering
private ArrayBlockingQueue<LastState> queue = new ArrayBlockingQueue<LastState>(1000, true);
public FileSyncThread() {
super("Last State Sync");
}
@Override
public void run() {
try {
slog.info("araqne log api: last state sync thread started");
// must do all flush job when stop signal'ed
while (!doStop || !queue.isEmpty()) {
try {
ArrayList<LastState> l = new ArrayList<LastState>();
// block waiting with timeout
LastState s = queue.poll(1, TimeUnit.SECONDS);
if (s != null) {
l.add(s);
queue.drainTo(l);
syncFiles(l);
slog.debug("araqne log api: sync'ed [{}] state", l.size());
}
} catch (Throwable e) {
slog.debug("araqne log api: last state sync error", e);
}
}
} finally {
slog.info("araqne log api: last state sync thread exit");
}
}
private void syncFiles(List<LastState> l) {
// merge state by logger name
Map<String, LastState> merge = new HashMap<String, LastState>();
for (LastState s : l) {
merge.put(s.getLoggerName(), s);
}
// flush files
for (LastState s : merge.values()) {
File f = getFilePath(s);
if (s instanceof DeleteState)
f.delete();
else
writeStateFile(s, f);
}
}
}
private File getFilePath(LastState s) {
String fileName = s.getLoggerName().replaceAll("\\\\", "\\$") + ".state";
return new File(dir, fileName);
}
private LastState readStateFile(File f) throws IOException {
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(f);
bis = new BufferedInputStream(fis);
JSONTokener tokener = new JSONTokener(new InputStreamReader(bis, "utf-8"));
Map<String, Object> m = JSONConverter.parse(new JSONObject(tokener));
if (m.get("last_log_date") != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ");
Date d = df.parse((String) m.get("last_log_date"), new ParsePosition(0));
m.put("last_log_date", d);
}
return PrimitiveConverter.parse(LastState.class, m);
} catch (ClassCastException e) {
throw new IOException("invalid json file [" + f.getAbsolutePath() + "]", e);
} catch (JSONException e) {
throw new IOException("invalid json file [" + f.getAbsolutePath() + "]", e);
} finally {
ensureClose(bis);
ensureClose(fis);
}
}
private void writeStateFile(LastState state, File f) {
// write tmp file first, and rename it
File tmp = new File(f.getAbsolutePath() + ".tmp");
FileOutputStream fos = null;
JSONWriter writer = null;
OutputStreamWriter ow = null;
BufferedWriter bw = null;
boolean success = false;
try {
fos = new FileOutputStream(tmp);
ow = new OutputStreamWriter(fos, "utf-8");
bw = new BufferedWriter(ow);
writer = new JSONWriter(bw);
JSONConverter.jsonize(PrimitiveConverter.serialize(state), writer);
success = true;
} catch (JSONException e) {
slog.error("araqne log api: cannot jsonize state [{}]", state.getLoggerName(), f.getAbsolutePath());
} catch (Throwable e) {
slog.error(
"araqne log api: cannot write state [" + state.getLoggerName() + "] to file [" + f.getAbsolutePath() + "]", e);
} finally {
ensureFlush(bw);
ensureFlush(ow);
ensureFsync(fos);
ensureClose(bw);
ensureClose(ow);
ensureClose(fos);
}
// prevent broken file writing caused by low disk space
if (success) {
boolean rename = (f.delete() || !f.exists()) && tmp.renameTo(f);
if (!rename) {
slog.error("araqne log api: cannot delete last state file [{}] to [{}]", tmp.getAbsolutePath(),
f.getAbsolutePath());
tmp.delete();
}
} else {
tmp.delete();
}
}
/**
* enforce fsync for ext4 (zero-length file problem)
*
* @see http://lwn.net/Articles/323169/
*/
private void ensureFsync(FileOutputStream o) {
if (o != null) {
try {
o.flush();
} catch (Throwable t) {
}
try {
o.getFD().sync();
} catch (Throwable t) {
}
}
}
private void ensureFlush(Writer w) {
if (w != null) {
try {
w.flush();
} catch (Throwable t) {
}
}
}
private void ensureClose(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException e) {
}
}
}
}